diff --git a/hazelcast-spring/src/test/java/com/hazelcast/spring/InvalidApplicationContextTest.java b/hazelcast-spring/src/test/java/com/hazelcast/spring/InvalidApplicationContextTest.java
new file mode 100644
index 000000000000..eb09b9b5b612
--- /dev/null
+++ b/hazelcast-spring/src/test/java/com/hazelcast/spring/InvalidApplicationContextTest.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2008-2020, Hazelcast, Inc. All Rights Reserved.
+ *
+ * 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
+ *
+ * http://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 com.hazelcast.spring;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.springframework.beans.factory.BeanDefinitionStoreException;
+import org.springframework.context.support.ClassPathXmlApplicationContext;
+
+public class InvalidApplicationContextTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @Test
+ public void shouldFailWhenLoadBalancerContainsClassNameAndImplementationAttribute() {
+ expectedException.expect(BeanDefinitionStoreException.class);
+ expectedException.expectMessage("Unexpected exception parsing XML document from class path resource [com/hazelcast/spring/"
+ + "customLoadBalancer-invalidApplicationContext.xml]; nested exception is "
+ + "java.lang.IllegalArgumentException: Exactly one of 'class-name' or 'implementation' attributes is "
+ + "required to create LoadBalancer!");
+
+ new ClassPathXmlApplicationContext("com\\hazelcast\\spring\\customLoadBalancer-invalidApplicationContext.xml");
+ }
+}
diff --git a/hazelcast-spring/src/test/java/com/hazelcast/spring/TestCustomLoadBalancerContext.java b/hazelcast-spring/src/test/java/com/hazelcast/spring/TestCustomLoadBalancerContext.java
new file mode 100644
index 000000000000..09aadc357829
--- /dev/null
+++ b/hazelcast-spring/src/test/java/com/hazelcast/spring/TestCustomLoadBalancerContext.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2008-2020, Hazelcast, Inc. All Rights Reserved.
+ *
+ * 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
+ *
+ * http://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 com.hazelcast.spring;
+
+import com.hazelcast.client.HazelcastClient;
+import com.hazelcast.client.LoadBalancer;
+import com.hazelcast.client.config.ClientConfig;
+import com.hazelcast.client.impl.clientside.HazelcastClientProxy;
+import com.hazelcast.client.test.CustomLoadBalancer;
+import com.hazelcast.core.Hazelcast;
+import com.hazelcast.test.annotation.QuickTest;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.springframework.test.context.ContextConfiguration;
+
+import javax.annotation.Resource;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(CustomSpringJUnit4ClassRunner.class)
+@ContextConfiguration(locations = {"customLoadBalancer-applicationContext.xml"})
+@Category(QuickTest.class)
+public class TestCustomLoadBalancerContext {
+
+ @Resource(name = "client1")
+ private HazelcastClientProxy client1;
+
+ @Resource(name = "client2")
+ private HazelcastClientProxy client2;
+
+ @BeforeClass
+ @AfterClass
+ public static void start() {
+ HazelcastClient.shutdownAll();
+ Hazelcast.shutdownAll();
+ }
+
+ @Test
+ public void testCustomLoadBalancer() {
+ ClientConfig config1 = client1.getClientConfig();
+ LoadBalancer loadBalancer1 = config1.getLoadBalancer();
+ assertTrue(loadBalancer1 instanceof CustomLoadBalancer);
+ assertEquals("default-name", ((CustomLoadBalancer) loadBalancer1).getName());
+
+ ClientConfig config2 = client2.getClientConfig();
+ LoadBalancer loadBalancer2 = config2.getLoadBalancer();
+ assertTrue(loadBalancer2 instanceof CustomLoadBalancer);
+ assertEquals("custom-balancer-name", ((CustomLoadBalancer) loadBalancer2).getName());
+ }
+}
diff --git a/hazelcast-spring/src/test/resources/com/hazelcast/spring/customLoadBalancer-applicationContext.xml b/hazelcast-spring/src/test/resources/com/hazelcast/spring/customLoadBalancer-applicationContext.xml
new file mode 100644
index 000000000000..0c013bf921db
--- /dev/null
+++ b/hazelcast-spring/src/test/resources/com/hazelcast/spring/customLoadBalancer-applicationContext.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+ classpath:/hazelcast-default.properties
+
+
+
+
+
+
+
+
+
+
+ ${cluster.members}
+
+
+
+
+
+
+
+
+ 127.0.0.1:5700
+ 127.0.0.1:5701
+
+
+
+
+
+
+ 127.0.0.1:5700
+ 127.0.0.1:5701
+
+
+
+
+
+
+
+
diff --git a/hazelcast-spring/src/test/resources/com/hazelcast/spring/customLoadBalancer-invalidApplicationContext.xml b/hazelcast-spring/src/test/resources/com/hazelcast/spring/customLoadBalancer-invalidApplicationContext.xml
new file mode 100644
index 000000000000..c66c3127838e
--- /dev/null
+++ b/hazelcast-spring/src/test/resources/com/hazelcast/spring/customLoadBalancer-invalidApplicationContext.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+ 127.0.0.1:5700
+ 127.0.0.1:5701
+
+
+
+
+
+
+
+
diff --git a/hazelcast/src/main/java/com/hazelcast/client/config/ClientConfig.java b/hazelcast/src/main/java/com/hazelcast/client/config/ClientConfig.java
index 3cbb591f1dd4..8f94e5553644 100644
--- a/hazelcast/src/main/java/com/hazelcast/client/config/ClientConfig.java
+++ b/hazelcast/src/main/java/com/hazelcast/client/config/ClientConfig.java
@@ -53,6 +53,7 @@
import static com.hazelcast.internal.config.DeclarativeConfigUtil.SYSPROP_CLIENT_CONFIG;
import static com.hazelcast.internal.config.DeclarativeConfigUtil.validateSuffixInSystemProperty;
import static com.hazelcast.internal.util.Preconditions.checkFalse;
+import static com.hazelcast.internal.util.Preconditions.checkHasText;
import static com.hazelcast.internal.util.Preconditions.isNotNull;
import static com.hazelcast.partition.strategy.StringPartitioningStrategy.getBaseName;
@@ -84,6 +85,11 @@ public class ClientConfig {
*/
private LoadBalancer loadBalancer;
+ /**
+ * Load balancer class name. Used internally with declarative configuration.
+ */
+ private String loadBalancerClassName;
+
/**
* List of listeners that Hazelcast will automatically add as a part of initialization process.
* Currently only supports {@link com.hazelcast.core.LifecycleListener}.
@@ -128,6 +134,7 @@ public ClientConfig(ClientConfig config) {
securityConfig = new ClientSecurityConfig(config.securityConfig);
networkConfig = new ClientNetworkConfig(config.networkConfig);
loadBalancer = config.loadBalancer;
+ loadBalancerClassName = config.loadBalancerClassName;
listenerConfigs = new LinkedList<>();
for (ListenerConfig listenerConfig : config.listenerConfigs) {
listenerConfigs.add(new ListenerConfig(listenerConfig));
@@ -599,7 +606,9 @@ public LoadBalancer getLoadBalancer() {
}
/**
- * Sets the {@link LoadBalancer}
+ * Sets the {@link LoadBalancer}.
+ *
+ * If a load balancer class name was set, it will be removed.
*
* @param loadBalancer {@link LoadBalancer}
* @return configured {@link com.hazelcast.client.config.ClientConfig} for chaining
@@ -607,9 +616,36 @@ public LoadBalancer getLoadBalancer() {
*/
public ClientConfig setLoadBalancer(LoadBalancer loadBalancer) {
this.loadBalancer = loadBalancer;
+ this.loadBalancerClassName = null;
+ return this;
+ }
+
+ /**
+ * Gets load balancer class name
+ *
+ * @return load balancer class name
+ * @see com.hazelcast.client.LoadBalancer
+ */
+ public String getLoadBalancerClassName() {
+ return loadBalancerClassName;
+ }
+
+ /**
+ * Sets load balancer class name.
+ *
+ * If a load balancer implementation was set, it will be removed.
+ *
+ * @param loadBalancerClassName {@link LoadBalancer}
+ * @return configured {@link com.hazelcast.client.config.ClientConfig} for chaining
+ * @see com.hazelcast.client.LoadBalancer
+ */
+ public ClientConfig setLoadBalancerClassName(@Nonnull String loadBalancerClassName) {
+ this.loadBalancerClassName = checkHasText(loadBalancerClassName, "Load balancer class name must contain text");
+ this.loadBalancer = null;
return this;
}
+
/**
* Gets the classLoader
*
@@ -923,7 +959,7 @@ public ClientConfig setMetricsConfig(@Nonnull ClientMetricsConfig metricsConfig)
@Override
public int hashCode() {
return Objects.hash(backupAckToClientEnabled, classLoader, clusterName, configPatternMatcher, connectionStrategyConfig,
- flakeIdGeneratorConfigMap, instanceName, labels, listenerConfigs, loadBalancer,
+ flakeIdGeneratorConfigMap, instanceName, labels, listenerConfigs, loadBalancer, loadBalancerClassName,
managedContext, metricsConfig, nativeMemoryConfig, nearCacheConfigMap, networkConfig, properties,
proxyFactoryConfigs, queryCacheConfigs, reliableTopicConfigMap, securityConfig, serializationConfig,
userCodeDeploymentConfig, userContext);
@@ -949,6 +985,7 @@ public boolean equals(Object obj) {
&& Objects.equals(flakeIdGeneratorConfigMap, other.flakeIdGeneratorConfigMap)
&& Objects.equals(instanceName, other.instanceName) && Objects.equals(labels, other.labels)
&& Objects.equals(listenerConfigs, other.listenerConfigs) && Objects.equals(loadBalancer, other.loadBalancer)
+ && Objects.equals(loadBalancerClassName, other.loadBalancerClassName)
&& Objects.equals(managedContext, other.managedContext) && Objects.equals(metricsConfig, other.metricsConfig)
&& Objects.equals(nativeMemoryConfig, other.nativeMemoryConfig)
&& Objects.equals(nearCacheConfigMap, other.nearCacheConfigMap)
@@ -970,6 +1007,7 @@ public String toString() {
+ ", securityConfig=" + securityConfig
+ ", networkConfig=" + networkConfig
+ ", loadBalancer=" + loadBalancer
+ + ", loadBalancerClassName=" + loadBalancerClassName
+ ", listenerConfigs=" + listenerConfigs
+ ", instanceName='" + instanceName + '\''
+ ", configPatternMatcher=" + configPatternMatcher
diff --git a/hazelcast/src/main/java/com/hazelcast/client/config/ClientConfigXmlGenerator.java b/hazelcast/src/main/java/com/hazelcast/client/config/ClientConfigXmlGenerator.java
index bdb576e245d5..dff6cd9afa0c 100644
--- a/hazelcast/src/main/java/com/hazelcast/client/config/ClientConfigXmlGenerator.java
+++ b/hazelcast/src/main/java/com/hazelcast/client/config/ClientConfigXmlGenerator.java
@@ -345,9 +345,14 @@ private static void loadBalancer(XmlGenerator gen, LoadBalancer loadBalancer) {
} else if (loadBalancer instanceof RoundRobinLB) {
type = "round-robin";
} else {
- throw new IllegalArgumentException("Unknown load-balancer type: " + loadBalancer);
+ type = "custom";
+ }
+
+ if ("custom".equals(type)) {
+ gen.node("load-balancer", loadBalancer.getClass().getName(), "type", type);
+ } else {
+ gen.node("load-balancer", null, "type", type);
}
- gen.node("load-balancer", null, "type", type);
}
private static void nearCaches(XmlGenerator gen, Map nearCacheMap) {
diff --git a/hazelcast/src/main/java/com/hazelcast/client/config/impl/ClientDomConfigProcessor.java b/hazelcast/src/main/java/com/hazelcast/client/config/impl/ClientDomConfigProcessor.java
index 32f7a5b28950..df5352b66230 100644
--- a/hazelcast/src/main/java/com/hazelcast/client/config/impl/ClientDomConfigProcessor.java
+++ b/hazelcast/src/main/java/com/hazelcast/client/config/impl/ClientDomConfigProcessor.java
@@ -114,7 +114,7 @@ public ClientDomConfigProcessor(boolean domLevel3, ClientConfig clientConfig) {
}
@Override
- public void buildConfig(Node rootNode) throws Exception {
+ public void buildConfig(Node rootNode) {
for (Node node : childElements(rootNode)) {
String nodeName = cleanNodeName(node);
if (occurrenceSet.contains(nodeName)) {
@@ -385,9 +385,16 @@ private void handleLoadBalancer(Node node) {
clientConfig.setLoadBalancer(new RandomLB());
} else if ("round-robin".equals(type)) {
clientConfig.setLoadBalancer(new RoundRobinLB());
+ } else if ("custom".equals(type)) {
+ String loadBalancerClassName = parseCustomLoadBalancerClassName(node);
+ clientConfig.setLoadBalancerClassName(loadBalancerClassName);
}
}
+ protected String parseCustomLoadBalancerClassName(Node node) {
+ return getTextContent(node);
+ }
+
private void handleNetwork(Node node) {
ClientNetworkConfig clientNetworkConfig = new ClientNetworkConfig();
for (Node child : childElements(node)) {
diff --git a/hazelcast/src/main/java/com/hazelcast/client/config/impl/YamlClientDomConfigProcessor.java b/hazelcast/src/main/java/com/hazelcast/client/config/impl/YamlClientDomConfigProcessor.java
index 54ddaf48b39a..4f8b4f1421bc 100644
--- a/hazelcast/src/main/java/com/hazelcast/client/config/impl/YamlClientDomConfigProcessor.java
+++ b/hazelcast/src/main/java/com/hazelcast/client/config/impl/YamlClientDomConfigProcessor.java
@@ -109,6 +109,11 @@ protected SerializationConfig parseSerialization(final Node node) {
return serializationConfig;
}
+ @Override
+ protected String parseCustomLoadBalancerClassName(Node node) {
+ return getAttribute(node, "class-name");
+ }
+
private void fillGlobalSerializer(Node child, SerializationConfig serializationConfig) {
GlobalSerializerConfig globalSerializerConfig = new GlobalSerializerConfig();
String attrClassName = getAttribute(child, "class-name");
diff --git a/hazelcast/src/main/java/com/hazelcast/client/impl/clientside/FailoverClientConfigSupport.java b/hazelcast/src/main/java/com/hazelcast/client/impl/clientside/FailoverClientConfigSupport.java
index 831d38ac8c8e..dbbb83b0c811 100644
--- a/hazelcast/src/main/java/com/hazelcast/client/impl/clientside/FailoverClientConfigSupport.java
+++ b/hazelcast/src/main/java/com/hazelcast/client/impl/clientside/FailoverClientConfigSupport.java
@@ -131,6 +131,9 @@ private static void checkValidAlternative(ClientConfig mainConfig, ClientConfig
if (notEqual(mainConfig.getLoadBalancer(), alternativeConfig.getLoadBalancer())) {
throwInvalidConfigurationException(mainClusterName, alterNativeClusterName, "loadBalancer");
}
+ if (notEqual(mainConfig.getLoadBalancerClassName(), alternativeConfig.getLoadBalancerClassName())) {
+ throwInvalidConfigurationException(mainClusterName, alterNativeClusterName, "loadBalancerClassName");
+ }
if (notEqual(mainConfig.getListenerConfigs(), alternativeConfig.getListenerConfigs())) {
throwInvalidConfigurationException(mainClusterName, alterNativeClusterName, "listeners");
}
diff --git a/hazelcast/src/main/java/com/hazelcast/client/impl/clientside/HazelcastClientInstanceImpl.java b/hazelcast/src/main/java/com/hazelcast/client/impl/clientside/HazelcastClientInstanceImpl.java
index b0448ceb9263..f44c53255d57 100644
--- a/hazelcast/src/main/java/com/hazelcast/client/impl/clientside/HazelcastClientInstanceImpl.java
+++ b/hazelcast/src/main/java/com/hazelcast/client/impl/clientside/HazelcastClientInstanceImpl.java
@@ -312,7 +312,15 @@ private void startMetrics() {
private LoadBalancer initLoadBalancer(ClientConfig config) {
LoadBalancer lb = config.getLoadBalancer();
if (lb == null) {
- lb = new RoundRobinLB();
+ if (config.getLoadBalancerClassName() != null) {
+ try {
+ return ClassLoaderUtil.newInstance(config.getClassLoader(), config.getLoadBalancerClassName());
+ } catch (Exception e) {
+ rethrow(e);
+ }
+ } else {
+ lb = new RoundRobinLB();
+ }
}
return lb;
}
diff --git a/hazelcast/src/main/resources/hazelcast-client-config-4.0.xsd b/hazelcast/src/main/resources/hazelcast-client-config-4.0.xsd
index 5dd18cdd6af8..1d4f0ad4a903 100644
--- a/hazelcast/src/main/resources/hazelcast-client-config-4.0.xsd
+++ b/hazelcast/src/main/resources/hazelcast-client-config-4.0.xsd
@@ -112,11 +112,11 @@
-
-
-
-
-
+
+
+
+
+
@@ -249,14 +249,19 @@