From a403a530d3d6e0cf0c76423c6774e17acbbfa680 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 18 Jan 2017 13:48:15 +0100 Subject: [PATCH] DATAREDIS-574 - Introduce dedicated RedisClientConfiguration for Jedis & Lettuce. We added Jedis-/LettuceClientConfiguration and mutable Jedis-/LettuceClientConfiguration to encapsulate Jedis/Lettuce specific client features. Those should be used for ConnectionFactory configuration. Additionally we added RedisStandaloneConfiguration next to the existing configurations for Sentinel and Cluster. Along the way we also removed superfluous test system properties and Gemfire specific hooks and truststore customization. Original Pull Request: #236 --- pom.xml | 4 - .../connection/RedisClusterConfiguration.java | 42 +- .../RedisSentinelConfiguration.java | 50 +- .../RedisStandaloneConfiguration.java | 127 ++++ .../DefaultJedisClientConfiguration.java | 131 ++++ .../jedis/JedisClientConfiguration.java | 414 +++++++++++++ .../jedis/JedisConnectionFactory.java | 574 +++++++++++++++--- .../DefaultLettuceClientConfiguration.java | 107 ++++ .../lettuce/LettuceClientConfiguration.java | 339 +++++++++++ .../lettuce/LettuceConnectionFactory.java | 448 +++++++++++--- .../data/redis/ConnectionFactoryTracker.java | 9 +- .../JedisClientConfigurationUnitTests.java | 105 ++++ ...edisConnectionFactoryIntegrationTests.java | 68 +++ ...ctionFactorySentinelIntegrationTests.java} | 40 +- .../JedisConnectionFactoryUnitTests.java | 219 ++++++- .../LettuceClientConfigurationUnitTests.java | 82 +++ .../LettuceConnectionFactoryUnitTests.java | 212 +++++++ .../core/AbstractOperationsTestParams.java | 8 +- .../core/ReactiveOperationsTestParams.java | 22 +- 19 files changed, 2773 insertions(+), 228 deletions(-) create mode 100644 src/main/java/org/springframework/data/redis/connection/RedisStandaloneConfiguration.java create mode 100644 src/main/java/org/springframework/data/redis/connection/jedis/DefaultJedisClientConfiguration.java create mode 100644 src/main/java/org/springframework/data/redis/connection/jedis/JedisClientConfiguration.java create mode 100644 src/main/java/org/springframework/data/redis/connection/lettuce/DefaultLettuceClientConfiguration.java create mode 100644 src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClientConfiguration.java create mode 100644 src/test/java/org/springframework/data/redis/connection/jedis/JedisClientConfigurationUnitTests.java create mode 100644 src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryIntegrationTests.java rename src/test/java/org/springframework/data/redis/connection/jedis/{JedisConnectionFactoryTests.java => JedisConnectionFactorySentinelIntegrationTests.java} (72%) create mode 100644 src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClientConfigurationUnitTests.java diff --git a/pom.xml b/pom.xml index f3272d4e9..734a2c4e7 100644 --- a/pom.xml +++ b/pom.xml @@ -208,10 +208,6 @@ **/*Tests.java **/*Test.java - - true - ${basedir}/src/test/resources/trusted.keystore - diff --git a/src/main/java/org/springframework/data/redis/connection/RedisClusterConfiguration.java b/src/main/java/org/springframework/data/redis/connection/RedisClusterConfiguration.java index e7a88c11e..729cf2305 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisClusterConfiguration.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisClusterConfiguration.java @@ -35,7 +35,7 @@ import org.springframework.util.StringUtils; * Configuration class used for setting up {@link RedisConnection} via {@link RedisConnectionFactory} using connecting * to Redis Cluster. Useful when setting up a high availability Redis * environment. - * + * * @author Christoph Strobl * @author Mark Paluch * @since 1.7 @@ -47,6 +47,7 @@ public class RedisClusterConfiguration { private Set clusterNodes; private Integer maxRedirects; + private String password; /** * Creates new {@link RedisClusterConfiguration}. @@ -57,16 +58,15 @@ public class RedisClusterConfiguration { /** * Creates {@link RedisClusterConfiguration} for given hostPort combinations. - * + * *
+	 * 
 	 * clusterHostAndPorts[0] = 127.0.0.1:23679
-	 * clusterHostAndPorts[1] = 127.0.0.1:23680
-	 * ...
-	 * 
+	 * clusterHostAndPorts[1] = 127.0.0.1:23680 ...
+	 * 
 	 * 
-	 * 
-	 * @param cluster must not be
-	 * {@literal null}.
+	 *
+	 * @param clusterNodes must not be {@literal null}.
 	 */
 	public RedisClusterConfiguration(Collection clusterNodes) {
 		this(new MapPropertySource("RedisClusterConfiguration", asMap(clusterNodes, -1, -1, null)));
@@ -74,7 +74,7 @@ public class RedisClusterConfiguration {
 
 	/**
 	 * Creates {@link RedisClusterConfiguration} looking up values in given {@link PropertySource}.
-	 * 
+	 *
 	 * 
 	 * 
 	 * spring.redis.cluster.nodes=127.0.0.1:23679,127.0.0.1:23680,127.0.0.1:23681
@@ -83,7 +83,7 @@ public class RedisClusterConfiguration {
 	 * spring.redis.cluster.password=foobar
 	 * 
 	 * 
- * + * * @param propertySource must not be {@literal null}. */ public RedisClusterConfiguration(PropertySource propertySource) { @@ -104,7 +104,7 @@ public class RedisClusterConfiguration { /** * Set {@literal cluster nodes} to connect to. - * + * * @param nodes must not be {@literal null}. */ public void setClusterNodes(Iterable nodes) { @@ -120,7 +120,7 @@ public class RedisClusterConfiguration { /** * Returns an {@link Collections#unmodifiableSet(Set)} of {@literal cluster nodes}. - * + * * @return {@link Set} of nodes. Never {@literal null}. */ public Set getClusterNodes() { @@ -129,7 +129,7 @@ public class RedisClusterConfiguration { /** * Add a cluster node to configuration. - * + * * @param node must not be {@literal null}. */ public void addClusterNode(RedisNode node) { @@ -179,6 +179,22 @@ public class RedisClusterConfiguration { } } + /** + * @return + * @since 2.0 + */ + public String getPassword() { + return password; + } + + /** + * @param password + * @since 2.0 + */ + public void setPassword(String password) { + this.password = password; + } + private RedisNode readHostAndPortFromString(String hostAndPort) { String[] args = split(hostAndPort, ":"); diff --git a/src/main/java/org/springframework/data/redis/connection/RedisSentinelConfiguration.java b/src/main/java/org/springframework/data/redis/connection/RedisSentinelConfiguration.java index e9f22fa2c..248f175d7 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisSentinelConfiguration.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisSentinelConfiguration.java @@ -16,6 +16,7 @@ package org.springframework.data.redis.connection; import static org.springframework.util.Assert.*; +import static org.springframework.util.Assert.hasText; import static org.springframework.util.StringUtils.*; import java.util.Collections; @@ -26,6 +27,7 @@ import java.util.Set; import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.PropertySource; +import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** @@ -35,6 +37,7 @@ import org.springframework.util.StringUtils; * * @author Christoph Strobl * @author Thomas Darimont + * @author Mark Paluch * @since 1.4 */ public class RedisSentinelConfiguration { @@ -44,6 +47,8 @@ public class RedisSentinelConfiguration { private NamedNode master; private Set sentinels; + private int database; + private String password; /** * Creates new {@link RedisSentinelConfiguration}. @@ -56,9 +61,7 @@ public class RedisSentinelConfiguration { * Creates {@link RedisSentinelConfiguration} for given hostPort combinations. * *
-	 * sentinelHostAndPorts[0] = 127.0.0.1:23679
-	 * sentinelHostAndPorts[1] = 127.0.0.1:23680
-	 * ...
+	 * sentinelHostAndPorts[0] = 127.0.0.1:23679 sentinelHostAndPorts[1] = 127.0.0.1:23680 ...
 	 * 
 	 * 
 	 * 
@@ -93,8 +96,8 @@ public class RedisSentinelConfiguration {
 		}
 
 		if (propertySource.containsProperty(REDIS_SENTINEL_NODES_CONFIG_PROPERTY)) {
-			appendSentinels(commaDelimitedListToSet(propertySource.getProperty(REDIS_SENTINEL_NODES_CONFIG_PROPERTY)
-					.toString()));
+			appendSentinels(
+					commaDelimitedListToSet(propertySource.getProperty(REDIS_SENTINEL_NODES_CONFIG_PROPERTY).toString()));
 		}
 	}
 
@@ -218,6 +221,43 @@ public class RedisSentinelConfiguration {
 		}
 	}
 
+	/**
+	 * @return
+	 * @since 2.0
+	 */
+	public int getDatabase() {
+		return database;
+	}
+
+	/**
+	 * Sets the index of the database used by this connection factory. Default is 0.
+	 *
+	 * @param index database index.
+	 * @since 2.0
+	 */
+	public void setDatabase(int index) {
+
+		Assert.isTrue(index >= 0, "invalid DB index (a positive index required)");
+
+		this.database = index;
+	}
+
+	/**
+	 * @return
+	 * @since 2.0
+	 */
+	public String getPassword() {
+		return password;
+	}
+
+	/**
+	 * @param password
+	 * @since 2.0
+	 */
+	public void setPassword(String password) {
+		this.password = password;
+	}
+
 	private RedisNode readHostAndPortFromString(String hostAndPort) {
 
 		String[] args = split(hostAndPort, ":");
diff --git a/src/main/java/org/springframework/data/redis/connection/RedisStandaloneConfiguration.java b/src/main/java/org/springframework/data/redis/connection/RedisStandaloneConfiguration.java
new file mode 100644
index 000000000..bc31f57e0
--- /dev/null
+++ b/src/main/java/org/springframework/data/redis/connection/RedisStandaloneConfiguration.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2017 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
+ *
+ *      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 org.springframework.data.redis.connection;
+
+import org.springframework.util.Assert;
+
+/**
+ * Configuration class used for setting up {@link RedisConnection} via {@link RedisConnectionFactory} using connecting
+ * to Redis.
+ *
+ * @author Mark Paluch
+ * @since 2.0
+ */
+public class RedisStandaloneConfiguration {
+
+	public static final String DEFAULT_HOST = "localhost";
+	public static final int DEFAULT_PORT = 6379;
+
+	private String hostName = DEFAULT_HOST;
+	private int port = DEFAULT_PORT;
+	private int database;
+	private String password;
+
+	/**
+	 * Create a new default {@link RedisStandaloneConfiguration}.
+	 */
+	public RedisStandaloneConfiguration() {}
+
+	/**
+	 * Create a new {@link RedisStandaloneConfiguration} given {@code hostName}.
+	 *
+	 * @param hostName must not be {@literal null} or empty.
+	 */
+	public RedisStandaloneConfiguration(String hostName) {
+		this(hostName, DEFAULT_PORT);
+	}
+
+	/**
+	 * Create a new {@link RedisStandaloneConfiguration} given {@code hostName} and {@code port}.
+	 *
+	 * @param hostName must not be {@literal null} or empty.
+	 * @param port a valid TCP port (1-65535).
+	 */
+	public RedisStandaloneConfiguration(String hostName, int port) {
+
+		Assert.hasText(hostName, "Host name must not be null or empty!");
+		Assert.isTrue(port >= 1 && port <= 65535,
+				() -> String.format("Port %d must be a valid TCP port in the range between 1-65535!", port));
+
+		this.hostName = hostName;
+		this.port = port;
+	}
+
+	/**
+	 * @return
+	 */
+	public String getHostName() {
+		return hostName;
+	}
+
+	/**
+	 * @return
+	 */
+	public int getPort() {
+		return port;
+	}
+
+	/**
+	 * @param hostName
+	 */
+	public void setHostName(String hostName) {
+		this.hostName = hostName;
+	}
+
+	/**
+	 * @param port
+	 */
+	public void setPort(int port) {
+		this.port = port;
+	}
+
+	/**
+	 * @return
+	 */
+	public int getDatabase() {
+		return database;
+	}
+
+	/**
+	 * Sets the index of the database used by this connection factory. Default is 0.
+	 *
+	 * @param index database index.
+	 */
+	public void setDatabase(int index) {
+
+		Assert.isTrue(index >= 0, "invalid DB index (a positive index required)");
+
+		this.database = index;
+	}
+
+	/**
+	 * @return
+	 */
+	public String getPassword() {
+		return password;
+	}
+
+	/**
+	 * @param password
+	 */
+	public void setPassword(String password) {
+		this.password = password;
+	}
+}
diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/DefaultJedisClientConfiguration.java b/src/main/java/org/springframework/data/redis/connection/jedis/DefaultJedisClientConfiguration.java
new file mode 100644
index 000000000..529f97e4f
--- /dev/null
+++ b/src/main/java/org/springframework/data/redis/connection/jedis/DefaultJedisClientConfiguration.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2017 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
+ *
+ *      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 org.springframework.data.redis.connection.jedis;
+
+import java.time.Duration;
+import java.util.Optional;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLSocketFactory;
+
+import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
+
+/**
+ * Default implementation of {@literal JedisClientConfiguration}.
+ *
+ * @author Mark Paluch
+ * @since 2.0
+ */
+class DefaultJedisClientConfiguration implements JedisClientConfiguration {
+
+	private final boolean useSsl;
+	private final Optional sslSocketFactory;
+	private final Optional sslParameters;
+	private final Optional hostnameVerifier;
+	private final boolean usePooling;
+	private final Optional poolConfig;
+	private final Optional clientName;
+	private final Duration readTimeout;
+	private final Duration connectTimeout;
+
+	DefaultJedisClientConfiguration(boolean useSsl, SSLSocketFactory sslSocketFactory, SSLParameters sslParameters,
+			HostnameVerifier hostnameVerifier, boolean usePooling, GenericObjectPoolConfig poolConfig, String clientName,
+			Duration readTimeout, Duration connectTimeout) {
+
+		this.useSsl = useSsl;
+		this.sslSocketFactory = Optional.ofNullable(sslSocketFactory);
+		this.sslParameters = Optional.ofNullable(sslParameters);
+		this.hostnameVerifier = Optional.ofNullable(hostnameVerifier);
+		this.usePooling = usePooling;
+		this.poolConfig = Optional.ofNullable(poolConfig);
+		this.clientName = Optional.ofNullable(clientName);
+		this.readTimeout = readTimeout;
+		this.connectTimeout = connectTimeout;
+	}
+
+	/* (non-Javadoc)
+	 * @see org.springframework.data.redis.connection.jedis.JedisClientConfiguration#useSsl()
+	 */
+	@Override
+	public boolean useSsl() {
+		return useSsl;
+	}
+
+	/* (non-Javadoc)
+	 * @see org.springframework.data.redis.connection.jedis.JedisClientConfiguration#getSslSocketFactory()
+	 */
+	@Override
+	public Optional getSslSocketFactory() {
+		return sslSocketFactory;
+	}
+
+	/* (non-Javadoc)
+	 * @see org.springframework.data.redis.connection.jedis.JedisClientConfiguration#getSslParameters()
+	 */
+	@Override
+	public Optional getSslParameters() {
+		return sslParameters;
+	}
+
+	/* (non-Javadoc)
+	 * @see org.springframework.data.redis.connection.jedis.JedisClientConfiguration#getHostnameVerifier()
+	 */
+	@Override
+	public Optional getHostnameVerifier() {
+		return hostnameVerifier;
+	}
+
+	/* (non-Javadoc)
+	 * @see org.springframework.data.redis.connection.jedis.JedisClientConfiguration#usePooling()
+	 */
+	@Override
+	public boolean usePooling() {
+		return usePooling;
+	}
+
+	/* (non-Javadoc)
+	 * @see org.springframework.data.redis.connection.jedis.JedisClientConfiguration#getPoolConfig()
+	 */
+	@Override
+	public Optional getPoolConfig() {
+		return poolConfig;
+	}
+
+	/* (non-Javadoc)
+	 * @see org.springframework.data.redis.connection.jedis.JedisClientConfiguration#getClientName()
+	 */
+	@Override
+	public Optional getClientName() {
+		return clientName;
+	}
+
+	/* (non-Javadoc)
+	 * @see org.springframework.data.redis.connection.jedis.JedisClientConfiguration#getReadTimeout()
+	 */
+	@Override
+	public Duration getReadTimeout() {
+		return readTimeout;
+	}
+
+	/* (non-Javadoc)
+	 * @see org.springframework.data.redis.connection.jedis.JedisClientConfiguration#getConnectTimeout()
+	 */
+	@Override
+	public Duration getConnectTimeout() {
+		return connectTimeout;
+	}
+}
diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClientConfiguration.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClientConfiguration.java
new file mode 100644
index 000000000..8beec65cf
--- /dev/null
+++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClientConfiguration.java
@@ -0,0 +1,414 @@
+/*
+ * Copyright 2017 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
+ *
+ *      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 org.springframework.data.redis.connection.jedis;
+
+import redis.clients.jedis.JedisPoolConfig;
+import redis.clients.jedis.Protocol;
+
+import java.net.SocketAddress;
+import java.time.Duration;
+import java.util.Optional;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLSocketFactory;
+
+import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
+import org.springframework.util.Assert;
+
+/**
+ * Redis client configuration for jedis. This configuration provides optional configuration elements such as
+ * {@link SSLSocketFactory} and {@link JedisPoolConfig} specific to jedis client features.
+ * 

+ * Providing optional elements allows a more specific configuration of the client: + *

    + *
  • Whether to use SSL
  • + *
  • Optional {@link SSLSocketFactory}
  • + *
  • Optional {@link SSLParameters}
  • + *
  • Optional {@link HostnameVerifier}
  • + *
  • Whether to use connection-pooling
  • + *
  • Optional {@link GenericObjectPoolConfig}
  • + *
  • Connect {@link Duration timeout}
  • + *
  • Read {@link Duration timeout}
  • + *
+ * + * @author Mark Paluch + * @since 2.0 + * @see redis.clients.jedis.Jedis + * @see org.springframework.data.redis.connection.RedisStandaloneConfiguration + * @see org.springframework.data.redis.connection.RedisSentinelConfiguration + * @see org.springframework.data.redis.connection.RedisClusterConfiguration + */ +public interface JedisClientConfiguration { + + /** + * @return {@literal true} to use SSL, {@literal false} to use unencrypted connections. + */ + boolean useSsl(); + + /** + * @return the optional {@link SSLSocketFactory}. + */ + Optional getSslSocketFactory(); + + /** + * @return the optional {@link SSLParameters}. + */ + Optional getSslParameters(); + + /** + * @return the optional {@link HostnameVerifier}. + */ + Optional getHostnameVerifier(); + + /** + * @return {@literal true} to use connection-pooling. + */ + boolean usePooling(); + + /** + * @return the optional {@link GenericObjectPoolConfig}. + */ + Optional getPoolConfig(); + + /** + * @return the optional client name to be set with {@code CLIENT SETNAME}. + */ + Optional getClientName(); + + /** + * @return the connection timeout. + * @see java.net.Socket#connect(SocketAddress, int) + */ + Duration getConnectTimeout(); + + /** + * @return the read timeout. + * @see java.net.Socket#setSoTimeout(int) + */ + Duration getReadTimeout(); + + /** + * Creates a new {@link JedisClientConfigurationBuilder} to build {@link JedisClientConfiguration} to be used with the + * jedis client. + * + * @return a new {@link JedisClientConfigurationBuilder} to build {@link JedisClientConfiguration}. + */ + static JedisClientConfigurationBuilder builder() { + return new DefaultJedisClientConfigurationBuilder(); + } + + /** + * Creates an empty {@link JedisClientConfiguration}. + * + * @return an empty {@link JedisClientConfiguration}. + */ + static JedisClientConfiguration create() { + return builder().build(); + } + + /** + * Builder for {@link JedisClientConfiguration}. + */ + interface JedisClientConfigurationBuilder { + + /** + * Enable SSL connections. + * + * @return {@link JedisSslClientConfigurationBuilder}. + */ + JedisSslClientConfigurationBuilder useSsl(); + + /** + * Use plaintext connections instead of SSL. + * + * @return {@link JedisSslClientConfigurationBuilder}. + */ + JedisClientConfigurationBuilder usePlaintext(); + + /** + * Enable connection-pooling. + * + * @return {@link JedisPoolingClientConfigurationBuilder}. + */ + JedisPoolingClientConfigurationBuilder usePooling(); + + /** + * Disable connection-pooling. + * + * @return {@link JedisClientConfigurationBuilder}. + */ + JedisClientConfigurationBuilder useUnpooledConnections(); + + /** + * Configure a {@code clientName} to be set with {@code CLIENT SETNAME}. + * + * @param clientName must not be {@literal null}. + * @return {@literal this} builder. + */ + JedisClientConfigurationBuilder clientName(String clientName); + + /** + * Configure a read timeout. + * + * @param readTimeout must not be {@literal null}. + * @return {@literal this} builder. + */ + JedisClientConfigurationBuilder readTimeout(Duration readTimeout); + + /** + * Configure a connection timeout. + * + * @param connectTimeout must not be {@literal null}. + * @return {@literal this} builder. + */ + JedisClientConfigurationBuilder connectTimeout(Duration connectTimeout); + + /** + * Build the {@link JedisClientConfiguration} with the configuration applied from this builder. + * + * @return a new {@link JedisClientConfiguration} object. + */ + JedisClientConfiguration build(); + } + + /** + * Builder for Pooling-related {@link JedisClientConfiguration}. + */ + interface JedisPoolingClientConfigurationBuilder { + + /** + * @param poolConfig must not be {@literal null}. + * @return {@literal this} builder. + */ + JedisPoolingClientConfigurationBuilder poolConfig(GenericObjectPoolConfig poolConfig); + + /** + * Return to {@link JedisClientConfigurationBuilder}. + * + * @return {@link JedisClientConfigurationBuilder}. + */ + JedisClientConfigurationBuilder and(); + + /** + * Build the {@link JedisClientConfiguration} with the configuration applied from this builder. + * + * @return a new {@link JedisClientConfiguration} object. + */ + JedisClientConfiguration build(); + } + + /** + * Builder for SSL-related {@link JedisClientConfiguration}. + */ + interface JedisSslClientConfigurationBuilder { + + /** + * @param sslSocketFactory must not be {@literal null}. + * @return {@literal this} builder. + */ + JedisSslClientConfigurationBuilder sslSocketFactory(SSLSocketFactory sslSocketFactory); + + /** + * @param sslParameters must not be {@literal null}. + * @return {@literal this} builder. + */ + JedisSslClientConfigurationBuilder sslParameters(SSLParameters sslParameters); + + /** + * @param hostnameVerifier must not be {@literal null}. + * @return {@literal this} builder. + */ + JedisSslClientConfigurationBuilder hostnameVerifier(HostnameVerifier hostnameVerifier); + + /** + * Return to {@link JedisClientConfigurationBuilder}. + * + * @return {@link JedisClientConfigurationBuilder}. + */ + JedisClientConfigurationBuilder and(); + + /** + * Build the {@link JedisClientConfiguration} with the configuration applied from this builder. + * + * @return a new {@link JedisClientConfiguration} object. + */ + JedisClientConfiguration build(); + } + + /** + * Default {@link JedisClientConfigurationBuilder} implementation to build an immutable + * {@link JedisClientConfiguration}. + */ + class DefaultJedisClientConfigurationBuilder implements JedisClientConfigurationBuilder, + JedisPoolingClientConfigurationBuilder, JedisSslClientConfigurationBuilder { + + private boolean useSsl; + private SSLSocketFactory sslSocketFactory; + private SSLParameters sslParameters; + private HostnameVerifier hostnameVerifier; + private boolean usePooling; + private GenericObjectPoolConfig poolConfig = new JedisPoolConfig(); + private String clientName; + private Duration readTimeout = Duration.ofMillis(Protocol.DEFAULT_TIMEOUT); + private Duration connectTimeout = Duration.ofMillis(Protocol.DEFAULT_TIMEOUT); + + private DefaultJedisClientConfigurationBuilder() {} + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.jedis.JedisClientConfiguration.JedisClientConfigurationBuilder#useSsl() + */ + @Override + public DefaultJedisClientConfigurationBuilder useSsl() { + + this.useSsl = true; + return this; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.jedis.JedisClientConfiguration.JedisClientConfigurationBuilder#usePlaintext() + */ + @Override + public JedisClientConfigurationBuilder usePlaintext() { + + this.useSsl = false; + return this; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.jedis.JedisClientConfiguration.JedisSslClientConfigurationBuilder#sslSocketFactory(javax.net.ssl.SSLSocketFactory) + */ + @Override + public DefaultJedisClientConfigurationBuilder sslSocketFactory(SSLSocketFactory sslSocketFactory) { + + Assert.notNull(sslSocketFactory, "SSLSocketFactory must not be null!"); + + this.sslSocketFactory = sslSocketFactory; + return this; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.jedis.JedisClientConfiguration.JedisSslClientConfigurationBuilder#sslParameters(javax.net.ssl.SSLParameters) + */ + @Override + public DefaultJedisClientConfigurationBuilder sslParameters(SSLParameters sslParameters) { + + Assert.notNull(sslParameters, "SSLParameters must not be null!"); + + this.sslParameters = sslParameters; + return this; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.jedis.JedisClientConfiguration.JedisSslClientConfigurationBuilder#hostnameVerifier(javax.net.ssl.HostnameVerifier) + */ + @Override + public DefaultJedisClientConfigurationBuilder hostnameVerifier(HostnameVerifier hostnameVerifier) { + + Assert.notNull(hostnameVerifier, "HostnameVerifier must not be null!"); + + this.hostnameVerifier = hostnameVerifier; + return this; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.jedis.JedisClientConfiguration.JedisClientConfigurationBuilder#usePooling() + */ + @Override + public DefaultJedisClientConfigurationBuilder usePooling() { + + this.usePooling = true; + return this; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.jedis.JedisClientConfiguration.JedisClientConfigurationBuilder#useUnpooledConnections() + */ + @Override + public JedisClientConfigurationBuilder useUnpooledConnections() { + + this.usePooling = false; + return this; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.jedis.JedisClientConfiguration.JedisPoolingClientConfigurationBuilder#poolConfig(org.apache.commons.pool2.impl.GenericObjectPoolConfig) + */ + @Override + public DefaultJedisClientConfigurationBuilder poolConfig(GenericObjectPoolConfig poolConfig) { + + Assert.notNull(poolConfig, "GenericObjectPoolConfig must not be null!"); + + this.poolConfig = poolConfig; + return this; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.jedis.JedisClientConfiguration.JedisPoolingClientConfigurationBuilder#and() + */ + @Override + public JedisClientConfigurationBuilder and() { + return this; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.jedis.JedisClientConfiguration.JedisClientConfigurationBuilder#clientName(java.lang.String) + */ + @Override + public JedisClientConfigurationBuilder clientName(String clientName) { + + Assert.hasText(clientName, "Client name must not be null or empty!"); + + this.clientName = clientName; + return this; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.jedis.JedisClientConfiguration.JedisClientConfigurationBuilder#readTimeout(java.time.Duration) + */ + @Override + public JedisClientConfigurationBuilder readTimeout(Duration readTimeout) { + + Assert.notNull(readTimeout, "Duration must not be null!"); + + this.readTimeout = readTimeout; + return this; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.jedis.JedisClientConfiguration.JedisClientConfigurationBuilder#connectTimeout(java.time.Duration) + */ + @Override + public JedisClientConfigurationBuilder connectTimeout(Duration connectTimeout) { + + Assert.notNull(connectTimeout, "Duration must not be null!"); + + this.connectTimeout = connectTimeout; + return this; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.jedis.JedisClientConfiguration.JedisClientConfigurationBuilder#build() + */ + @Override + public JedisClientConfiguration build() { + + return new DefaultJedisClientConfiguration(useSsl, sslSocketFactory, sslParameters, hostnameVerifier, usePooling, + poolConfig, clientName, readTimeout, connectTimeout); + } + } + +} diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactory.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactory.java index 023f281e0..be4f2a4de 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactory.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactory.java @@ -15,6 +15,7 @@ */ package org.springframework.data.redis.connection.jedis; +import redis.clients.jedis.Client; import redis.clients.jedis.HostAndPort; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisCluster; @@ -26,12 +27,18 @@ import redis.clients.jedis.Protocol; import redis.clients.util.Pool; import java.lang.reflect.Method; +import java.time.Duration; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; +import java.util.Optional; import java.util.Set; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSocketFactory; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; @@ -51,19 +58,31 @@ import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisNode; import org.springframework.data.redis.connection.RedisSentinelConfiguration; import org.springframework.data.redis.connection.RedisSentinelConnection; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; /** * Connection factory creating Jedis based connections. + *

+ * {@link JedisConnectionFactory} should be configured using an environmental configuration and the + * {@link JedisClientConfiguration client configuration}. Jedis supports the following environmental configurations: + *

    + *
  • {@link RedisStandaloneConfiguration}
  • + *
  • {@link RedisSentinelConfiguration}
  • + *
  • {@link RedisClusterConfiguration}
  • + *
* * @author Costin Leau * @author Thomas Darimont * @author Christoph Strobl * @author Mark Paluch * @author Fu Jian + * @see JedisClientConfiguration + * @see Jedis */ public class JedisConnectionFactory implements InitializingBean, DisposableBean, RedisConnectionFactory { @@ -72,7 +91,6 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, JedisConverters.exceptionConverter()); private static final Method SET_TIMEOUT_METHOD; - private static final Method GET_TIMEOUT_METHOD; static { @@ -83,27 +101,15 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, setTimeoutMethodCandidate = ReflectionUtils.findMethod(JedisShardInfo.class, "setSoTimeout", int.class); } SET_TIMEOUT_METHOD = setTimeoutMethodCandidate; - - Method getTimeoutMethodCandidate = ReflectionUtils.findMethod(JedisShardInfo.class, "getTimeout"); - if (getTimeoutMethodCandidate == null) { - getTimeoutMethodCandidate = ReflectionUtils.findMethod(JedisShardInfo.class, "getSoTimeout"); - } - - GET_TIMEOUT_METHOD = getTimeoutMethodCandidate; } + private final JedisClientConfiguration clientConfiguration; private JedisShardInfo shardInfo; - private String hostName = "localhost"; - private int port = Protocol.DEFAULT_PORT; - private int timeout = Protocol.DEFAULT_TIMEOUT; - private String password; - private boolean usePool = true; - private boolean useSsl = false; + private boolean providedShardInfo = false; private Pool pool; - private JedisPoolConfig poolConfig = new JedisPoolConfig(); - private int dbIndex = 0; - private String clientName; private boolean convertPipelineAndTxResults = true; + private RedisStandaloneConfiguration standaloneConfig = new RedisStandaloneConfiguration("localhost", + Protocol.DEFAULT_PORT); private RedisSentinelConfiguration sentinelConfig; private RedisClusterConfiguration clusterConfig; private JedisCluster cluster; @@ -113,16 +119,38 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, * Constructs a new JedisConnectionFactory instance with default settings (default connection pooling, no * shard information). */ - public JedisConnectionFactory() {} + public JedisConnectionFactory() { + this(new MutableJedisClientConfiguration()); + } + + /** + * Constructs a new {@link JedisConnectionFactory} instance given {@link JedisClientConfiguration}. + * + * @param clientConfig must not be {@literal null} + * @since 2.0 + */ + private JedisConnectionFactory(JedisClientConfiguration clientConfig) { + + Assert.notNull(clientConfig, "JedisClientConfiguration must not be null!"); + + this.clientConfiguration = clientConfig; + } /** * Constructs a new JedisConnectionFactory instance. Will override the other connection parameters passed * to the factory. * * @param shardInfo shard information + * @deprecated since 2.0, configure Jedis with {@link JedisClientConfiguration} and + * {@link RedisStandaloneConfiguration}. */ + @Deprecated public JedisConnectionFactory(JedisShardInfo shardInfo) { + + this(MutableJedisClientConfiguration.create(shardInfo)); + this.shardInfo = shardInfo; + this.providedShardInfo = true; } /** @@ -138,11 +166,11 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, * Constructs a new {@link JedisConnectionFactory} instance using the given {@link JedisPoolConfig} applied to * {@link JedisSentinelPool}. * - * @param sentinelConfig + * @param sentinelConfig must not be {@literal null}. * @since 1.4 */ public JedisConnectionFactory(RedisSentinelConfiguration sentinelConfig) { - this(sentinelConfig, null); + this(sentinelConfig, new MutableJedisClientConfiguration()); } /** @@ -154,31 +182,97 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, * @since 1.4 */ public JedisConnectionFactory(RedisSentinelConfiguration sentinelConfig, JedisPoolConfig poolConfig) { + this.sentinelConfig = sentinelConfig; - this.poolConfig = poolConfig != null ? poolConfig : new JedisPoolConfig(); + this.clientConfiguration = MutableJedisClientConfiguration + .create(poolConfig != null ? poolConfig : new JedisPoolConfig()); } /** * Constructs a new {@link JedisConnectionFactory} instance using the given {@link RedisClusterConfiguration} applied * to create a {@link JedisCluster}. * - * @param clusterConfig + * @param clusterConfig must not be {@literal null}. * @since 1.7 */ public JedisConnectionFactory(RedisClusterConfiguration clusterConfig) { - this.clusterConfig = clusterConfig; + this(clusterConfig, new MutableJedisClientConfiguration()); } /** * Constructs a new {@link JedisConnectionFactory} instance using the given {@link RedisClusterConfiguration} applied * to create a {@link JedisCluster}. * - * @param clusterConfig + * @param clusterConfig must not be {@literal null}. * @since 1.7 */ public JedisConnectionFactory(RedisClusterConfiguration clusterConfig, JedisPoolConfig poolConfig) { + + Assert.notNull(clusterConfig, "RedisClusterConfiguration must not be null!"); + + this.clusterConfig = clusterConfig; + this.clientConfiguration = MutableJedisClientConfiguration.create(poolConfig); + } + + /** + * Constructs a new {@link JedisConnectionFactory} instance using the given {@link RedisStandaloneConfiguration}. + * + * @param standaloneConfig must not be {@literal null}. + * @since 2.0 + */ + public JedisConnectionFactory(RedisStandaloneConfiguration standaloneConfig) { + this(standaloneConfig, new MutableJedisClientConfiguration()); + } + + /** + * Constructs a new {@link JedisConnectionFactory} instance using the given {@link RedisStandaloneConfiguration} and + * {@link JedisClientConfiguration}. + * + * @param standaloneConfig must not be {@literal null}. + * @param clientConfig must not be {@literal null}. + * @since 2.0 + */ + public JedisConnectionFactory(RedisStandaloneConfiguration standaloneConfig, JedisClientConfiguration clientConfig) { + + this(clientConfig); + + Assert.notNull(standaloneConfig, "RedisStandaloneConfiguration must not be null!"); + + this.standaloneConfig = standaloneConfig; + } + + /** + * Constructs a new {@link JedisConnectionFactory} instance using the given {@link RedisSentinelConfiguration} and + * {@link JedisClientConfiguration}. + * + * @param sentinelConfig must not be {@literal null}. + * @param clientConfig must not be {@literal null}. + * @since 2.0 + */ + public JedisConnectionFactory(RedisSentinelConfiguration sentinelConfig, JedisClientConfiguration clientConfig) { + + this(clientConfig); + + Assert.notNull(sentinelConfig, "RedisSentinelConfiguration must not be null!"); + + this.sentinelConfig = sentinelConfig; + } + + /** + * Constructs a new {@link JedisConnectionFactory} instance using the given {@link RedisClusterConfiguration} and + * {@link JedisClientConfiguration}. + * + * @param clusterConfig must not be {@literal null}. + * @param clientConfig must not be {@literal null}. + * @since 2.0 + */ + public JedisConnectionFactory(RedisClusterConfiguration clusterConfig, JedisClientConfiguration clientConfig) { + + this(clientConfig); + + Assert.notNull(clusterConfig, "RedisClusterConfiguration must not be null!"); + this.clusterConfig = clusterConfig; - this.poolConfig = poolConfig; } /** @@ -190,11 +284,11 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, protected Jedis fetchJedisConnector() { try { - if (usePool && pool != null) { + if (getUsePool() && pool != null) { return pool.getResource(); } - Jedis jedis = new Jedis(getShardInfo()); + Jedis jedis = createJedis(); // force initialization (see Jedis issue #82) jedis.connect(); @@ -205,6 +299,25 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, } } + private Jedis createJedis() { + + if (providedShardInfo) { + return new Jedis(getShardInfo()); + } + + Jedis jedis = new Jedis(getHostName(), getPort(), getConnectTimeout(), getReadTimeout(), isUseSsl(), + clientConfiguration.getSslSocketFactory().orElse(null), // + clientConfiguration.getSslParameters().orElse(null), // + clientConfiguration.getHostnameVerifier().orElse(null)); + + Client client = jedis.getClient(); + + client.setPassword(getPassword()); + client.setDb(getDatabase()); + + return jedis; + } + /** * Post process a newly retrieved connection. Useful for decorating or executing initialization commands on a new * connection. This implementation simply returns the connection. @@ -221,23 +334,33 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() */ public void afterPropertiesSet() { - if (shardInfo == null) { - shardInfo = new JedisShardInfo(hostName, port); - if (StringUtils.hasLength(password)) { - shardInfo.setPassword(password); + if (shardInfo == null && clientConfiguration instanceof MutableJedisClientConfiguration) { + + providedShardInfo = false; + shardInfo = new JedisShardInfo(getHostName(), getPort(), isUseSsl(), // + clientConfiguration.getSslSocketFactory().orElse(null), // + clientConfiguration.getSslParameters().orElse(null), // + clientConfiguration.getHostnameVerifier().orElse(null)); + + if (StringUtils.hasLength(getPassword())) { + shardInfo.setPassword(getPassword()); } - if (timeout > 0) { - setTimeoutOn(shardInfo, timeout); + int readTimeout = getReadTimeout(); + + if (readTimeout > 0) { + setTimeoutOn(shardInfo, readTimeout); } + + getMutableConfiguration().setShardInfo(shardInfo); } - if (usePool && clusterConfig == null) { + if (getUsePool() && !isRedisClusterAware()) { this.pool = createPool(); } - if (clusterConfig != null) { + if (isRedisClusterAware()) { this.cluster = createCluster(); } } @@ -258,9 +381,10 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, * @since 1.4 */ protected Pool createRedisSentinelPool(RedisSentinelConfiguration config) { + + GenericObjectPoolConfig poolConfig = getPoolConfig() != null ? getPoolConfig() : new JedisPoolConfig(); return new JedisSentinelPool(config.getMaster().getName(), convertToJedisSentinelSet(config.getSentinels()), - getPoolConfig() != null ? getPoolConfig() : new JedisPoolConfig(), getTimeoutFrom(getShardInfo()), - getShardInfo().getPassword(), Protocol.DEFAULT_DATABASE, clientName); + poolConfig, getConnectTimeout(), getReadTimeout(), getPassword(), Protocol.DEFAULT_DATABASE, getClientName()); } /** @@ -271,13 +395,16 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, */ protected Pool createRedisPool() { - return new JedisPool(getPoolConfig(), getShardInfo().getHost(), getShardInfo().getPort(), - getTimeoutFrom(getShardInfo()), getShardInfo().getPassword(), Protocol.DEFAULT_DATABASE, clientName, useSsl); + return new JedisPool(getPoolConfig(), getHostName(), getPort(), getConnectTimeout(), getReadTimeout(), + getPassword(), Protocol.DEFAULT_DATABASE, getClientName(), isUseSsl(), + clientConfiguration.getSslSocketFactory().orElse(null), // + clientConfiguration.getSslParameters().orElse(null), // + clientConfiguration.getHostnameVerifier().orElse(null)); } private JedisCluster createCluster() { - JedisCluster cluster = createCluster(this.clusterConfig, this.poolConfig); + JedisCluster cluster = createCluster(this.clusterConfig, getPoolConfig()); this.clusterCommandExecutor = new ClusterCommandExecutor( new JedisClusterConnection.JedisClusterTopologyProvider(cluster), new JedisClusterConnection.JedisClusterNodeResourceProvider(cluster), EXCEPTION_TRANSLATION); @@ -301,11 +428,14 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, hostAndPort.add(new HostAndPort(node.getHost(), node.getPort())); } - int redirects = clusterConfig.getMaxRedirects() != null ? clusterConfig.getMaxRedirects().intValue() : 5; + int redirects = clusterConfig.getMaxRedirects() != null ? clusterConfig.getMaxRedirects() : 5; + + int connectTimeout = getConnectTimeout(); + int readTimeout = getReadTimeout(); return StringUtils.hasText(getPassword()) - ? new JedisCluster(hostAndPort, timeout, timeout, redirects, password, poolConfig) - : new JedisCluster(hostAndPort, timeout, redirects, poolConfig); + ? new JedisCluster(hostAndPort, connectTimeout, readTimeout, redirects, getPassword(), poolConfig) + : new JedisCluster(hostAndPort, connectTimeout, readTimeout, redirects, poolConfig); } /* @@ -313,7 +443,9 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, * @see org.springframework.beans.factory.DisposableBean#destroy() */ public void destroy() { - if (usePool && pool != null) { + + if (getUsePool() && pool != null) { + try { pool.destroy(); } catch (Exception ex) { @@ -321,12 +453,15 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, } pool = null; } + if (cluster != null) { + try { cluster.close(); } catch (Exception ex) { log.warn("Cannot properly close Jedis cluster", ex); } + try { clusterCommandExecutor.destroy(); } catch (Exception ex) { @@ -341,13 +476,14 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, */ public RedisConnection getConnection() { - if (cluster != null) { + if (isRedisClusterAware()) { return getClusterConnection(); } Jedis jedis = fetchJedisConnector(); - JedisConnection connection = (usePool ? new JedisConnection(jedis, pool, dbIndex, clientName) - : new JedisConnection(jedis, null, dbIndex, clientName)); + String clientName = clientConfiguration.getClientName().orElse(null); + JedisConnection connection = (getUsePool() ? new JedisConnection(jedis, pool, getDatabase(), clientName) + : new JedisConnection(jedis, null, getDatabase(), clientName)); connection.setConvertPipelineAndTxResults(convertPipelineAndTxResults); return postProcessConnection(connection); } @@ -359,7 +495,7 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, @Override public RedisClusterConnection getClusterConnection() { - if (cluster == null) { + if (!isRedisClusterAware()) { throw new InvalidDataAccessApiUsageException("Cluster is not configured!"); } return new JedisClusterConnection(cluster, clusterCommandExecutor); @@ -374,31 +510,23 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, } /** - * Returns the Redis hostName. + * Returns the Redis hostname. * * @return the hostName. */ public String getHostName() { - return hostName; + return standaloneConfig.getHostName(); } /** - * Sets the Redis hostName. + * Sets the Redis hostname. * - * @param hostName the hostName to set. + * @param hostName the hostname to set. + * @deprecated since 2.0, configure the hostname using {@link RedisStandaloneConfiguration}. */ + @Deprecated public void setHostName(String hostName) { - this.hostName = hostName; - } - - /** - * Sets whether to use SSL. - * - * @param useSsl {@literal true} to use SSL. - * @since 1.8 - */ - public void setUseSsl(boolean useSsl) { - this.useSsl = useSsl; + standaloneConfig.setHostName(hostName); } /** @@ -408,7 +536,20 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, * @since 1.8 */ public boolean isUseSsl() { - return useSsl; + return clientConfiguration.useSsl(); + } + + /** + * Sets whether to use SSL. + * + * @param useSsl {@literal true} to use SSL. + * @since 1.8 + * @deprecated since 2.0, configure the SSL usage with {@link JedisClientConfiguration}. + * @throws IllegalStateException if {@link JedisClientConfiguration} is immutable. + */ + @Deprecated + public void setUseSsl(boolean useSsl) { + getMutableConfiguration().setUseSsl(useSsl); } /** @@ -417,16 +558,39 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, * @return password for authentication. */ public String getPassword() { - return password; + + if (isRedisSentinelAware()) { + return sentinelConfig.getPassword(); + } + + if (isRedisClusterAware()) { + return clusterConfig.getPassword(); + } + + return standaloneConfig.getPassword(); } /** * Sets the password used for authenticating with the Redis server. * * @param password the password to set. + * @deprecated since 2.0, configure the password using {@link RedisStandaloneConfiguration}, + * {@link RedisSentinelConfiguration} or {@link RedisClusterConfiguration}. */ + @Deprecated public void setPassword(String password) { - this.password = password; + + if (isRedisSentinelAware()) { + sentinelConfig.setPassword(password); + return; + } + + if (isRedisClusterAware()) { + clusterConfig.setPassword(password); + return; + } + + standaloneConfig.setPassword(password); } /** @@ -435,23 +599,27 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, * @return the Redis port. */ public int getPort() { - return port; + return standaloneConfig.getPort(); } /** * Sets the port used to connect to the Redis instance. * * @param port the Redis port. + * @deprecated since 2.0, configure the port using {@link RedisStandaloneConfiguration}. */ + @Deprecated public void setPort(int port) { - this.port = port; + standaloneConfig.setPort(port); } /** * Returns the shardInfo. * * @return the shardInfo. + * @deprecated since 2.0. */ + @Deprecated public JedisShardInfo getShardInfo() { return shardInfo; } @@ -460,9 +628,16 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, * Sets the shard info for this factory. * * @param shardInfo the shardInfo to set. + * @deprecated since 2.0, configure the individual properties from {@link JedisShardInfo} using + * {@link JedisClientConfiguration}. + * @throws IllegalStateException if {@link JedisClientConfiguration} is immutable. */ + @Deprecated public void setShardInfo(JedisShardInfo shardInfo) { + this.shardInfo = shardInfo; + this.providedShardInfo = true; + getMutableConfiguration().setShardInfo(shardInfo); } /** @@ -471,16 +646,21 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, * @return the timeout. */ public int getTimeout() { - return timeout; + return getReadTimeout(); } /** * Sets the timeout. * * @param timeout the timeout to set. + * @deprecated since 2.0, configure the timeout using {@link JedisClientConfiguration}. + * @throws IllegalStateException if {@link JedisClientConfiguration} is immutable. */ + @Deprecated public void setTimeout(int timeout) { - this.timeout = timeout; + + getMutableConfiguration().setReadTimeout(Duration.ofMillis(timeout)); + getMutableConfiguration().setConnectTimeout(Duration.ofMillis(timeout)); } /** @@ -489,16 +669,19 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, * @return the use of connection pooling. */ public boolean getUsePool() { - return usePool; + return clientConfiguration.usePooling(); } /** * Turns on or off the use of connection pooling. * * @param usePool the usePool to set. + * @deprecated since 2.0, configure pooling usage with {@link JedisClientConfiguration}. + * @throws IllegalStateException if {@link JedisClientConfiguration} is immutable. */ + @Deprecated public void setUsePool(boolean usePool) { - this.usePool = usePool; + getMutableConfiguration().setUsePooling(usePool); } /** @@ -506,17 +689,20 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, * * @return the poolConfig */ - public JedisPoolConfig getPoolConfig() { - return poolConfig; + public GenericObjectPoolConfig getPoolConfig() { + return clientConfiguration.getPoolConfig().orElse(null); } /** * Sets the pool configuration for this factory. * * @param poolConfig the poolConfig to set. + * @deprecated since 2.0, configure {@link JedisPoolConfig} using {@link JedisClientConfiguration}. + * @throws IllegalStateException if {@link JedisClientConfiguration} is immutable. */ + @Deprecated public void setPoolConfig(JedisPoolConfig poolConfig) { - this.poolConfig = poolConfig; + getMutableConfiguration().setPoolConfig(poolConfig); } /** @@ -525,17 +711,32 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, * @return the database index. */ public int getDatabase() { - return dbIndex; + + if (isRedisSentinelAware()) { + return sentinelConfig.getDatabase(); + } + + return standaloneConfig.getDatabase(); } /** * Sets the index of the database used by this connection factory. Default is 0. * * @param index database index. + * @deprecated since 2.0, configure the client name using {@link RedisSentinelConfiguration} or + * {@link RedisStandaloneConfiguration}. */ + @Deprecated public void setDatabase(int index) { + Assert.isTrue(index >= 0, "invalid DB index (a positive index required)"); - this.dbIndex = index; + + if (isRedisSentinelAware()) { + sentinelConfig.setDatabase(index); + return; + } + + standaloneConfig.setDatabase(index); } /** @@ -545,7 +746,7 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, * @since 1.8 */ public String getClientName() { - return clientName; + return clientConfiguration.getClientName().orElse(null); } /** @@ -553,9 +754,44 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, * * @param clientName the client name. * @since 1.8 + * @deprecated since 2.0, configure the client name using {@link JedisClientConfiguration}. + * @throws IllegalStateException if {@link JedisClientConfiguration} is immutable. */ + @Deprecated public void setClientName(String clientName) { - this.clientName = clientName; + this.getMutableConfiguration().setClientName(clientName); + } + + /** + * @return the {@link JedisClientConfiguration}. + * @since 2.0 + */ + public JedisClientConfiguration getClientConfiguration() { + return clientConfiguration; + } + + /** + * @return the {@link RedisStandaloneConfiguration}. + * @since 2.0 + */ + public RedisStandaloneConfiguration getStandaloneConfiguration() { + return standaloneConfig; + } + + /** + * @return the {@link RedisStandaloneConfiguration}, may be {@literal null}. + * @since 2.0 + */ + public RedisSentinelConfiguration getSentinelConfiguration() { + return sentinelConfig; + } + + /** + * @return the {@link RedisClusterConfiguration}, may be {@literal null}. + * @since 2.0 + */ + public RedisClusterConfiguration getClusterConfiguration() { + return clusterConfig; } /** @@ -588,6 +824,14 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, return sentinelConfig != null; } + /** + * @return true when {@link RedisClusterConfiguration} is present. + * @since 2.0 + */ + public boolean isRedisClusterAware() { + return clusterConfig != null; + } + /* (non-Javadoc) * @see org.springframework.data.redis.connection.RedisConnectionFactory#getSentinelConnection() */ @@ -607,7 +851,7 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, for (RedisNode node : this.sentinelConfig.getSentinels()) { - Jedis jedis = new Jedis(node.getHost(), node.getPort()); + Jedis jedis = new Jedis(node.getHost(), node.getPort(), getConnectTimeout(), getReadTimeout()); if (jedis.ping().equalsIgnoreCase("pong")) { @@ -635,18 +879,176 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, } private void potentiallySetClientName(Jedis jedis) { - - if (StringUtils.hasText(clientName)) { - jedis.clientSetname(clientName); - } + clientConfiguration.getClientName().ifPresent(jedis::clientSetname); } private void setTimeoutOn(JedisShardInfo shardInfo, int timeout) { ReflectionUtils.invokeMethod(SET_TIMEOUT_METHOD, shardInfo, timeout); } - private int getTimeoutFrom(JedisShardInfo shardInfo) { - return (Integer) ReflectionUtils.invokeMethod(GET_TIMEOUT_METHOD, shardInfo); + private int getReadTimeout() { + return Math.toIntExact(clientConfiguration.getReadTimeout().toMillis()); } + private int getConnectTimeout() { + return Math.toIntExact(clientConfiguration.getConnectTimeout().toMillis()); + } + + private MutableJedisClientConfiguration getMutableConfiguration() { + + Assert.state(clientConfiguration instanceof MutableJedisClientConfiguration, + () -> String.format("Client configuration must be instance of MutableJedisClientConfiguration but is %s", + ClassUtils.getShortName(clientConfiguration.getClass()))); + + return (MutableJedisClientConfiguration) clientConfiguration; + } + + static class MutableJedisClientConfiguration implements JedisClientConfiguration { + + private boolean useSsl; + private SSLSocketFactory sslSocketFactory; + private SSLParameters sslParameters; + private HostnameVerifier hostnameVerifier; + private boolean usePooling = true; + private GenericObjectPoolConfig poolConfig = new JedisPoolConfig(); + private String clientName; + private Duration readTimeout = Duration.ofMillis(Protocol.DEFAULT_TIMEOUT); + private Duration connectTimeout = Duration.ofMillis(Protocol.DEFAULT_TIMEOUT); + + public static JedisClientConfiguration create(JedisShardInfo shardInfo) { + + MutableJedisClientConfiguration configuration = new MutableJedisClientConfiguration(); + + configuration.setShardInfo(shardInfo); + + return configuration; + } + + public static JedisClientConfiguration create(GenericObjectPoolConfig jedisPoolConfig) { + + MutableJedisClientConfiguration configuration = new MutableJedisClientConfiguration(); + + configuration.setPoolConfig(jedisPoolConfig); + + return configuration; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.jedis.JedisClientConfiguration#useSsl() + */ + @Override + public boolean useSsl() { + return useSsl; + } + + public void setUseSsl(boolean useSsl) { + this.useSsl = useSsl; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.jedis.JedisClientConfiguration#getSslSocketFactory() + */ + @Override + public Optional getSslSocketFactory() { + return Optional.ofNullable(sslSocketFactory); + } + + public void setSslSocketFactory(SSLSocketFactory sslSocketFactory) { + this.sslSocketFactory = sslSocketFactory; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.jedis.JedisClientConfiguration#getSslParameters() + */ + @Override + public Optional getSslParameters() { + return Optional.ofNullable(sslParameters); + } + + public void setSslParameters(SSLParameters sslParameters) { + this.sslParameters = sslParameters; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.jedis.JedisClientConfiguration#getHostnameVerifier() + */ + @Override + public Optional getHostnameVerifier() { + return Optional.ofNullable(hostnameVerifier); + } + + public void setHostnameVerifier(HostnameVerifier hostnameVerifier) { + this.hostnameVerifier = hostnameVerifier; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.jedis.JedisClientConfiguration#usePooling() + */ + @Override + public boolean usePooling() { + return usePooling; + } + + public void setUsePooling(boolean usePooling) { + this.usePooling = usePooling; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.jedis.JedisClientConfiguration#getPoolConfig() + */ + @Override + public Optional getPoolConfig() { + return Optional.ofNullable(poolConfig); + } + + public void setPoolConfig(GenericObjectPoolConfig poolConfig) { + this.poolConfig = poolConfig; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.jedis.JedisClientConfiguration#getClientName() + */ + @Override + public Optional getClientName() { + return Optional.ofNullable(clientName); + } + + public void setClientName(String clientName) { + this.clientName = clientName; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.jedis.JedisClientConfiguration#getReadTimeout() + */ + @Override + public Duration getReadTimeout() { + return readTimeout; + } + + public void setReadTimeout(Duration readTimeout) { + this.readTimeout = readTimeout; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.jedis.JedisClientConfiguration#getConnectTimeout() + */ + @Override + public Duration getConnectTimeout() { + return connectTimeout; + } + + public void setConnectTimeout(Duration connectTimeout) { + this.connectTimeout = connectTimeout; + } + + public void setShardInfo(JedisShardInfo shardInfo) { + + setSslSocketFactory(shardInfo.getSslSocketFactory()); + setSslParameters(shardInfo.getSslParameters()); + setHostnameVerifier(shardInfo.getHostnameVerifier()); + setUseSsl(shardInfo.getSsl()); + setConnectTimeout(Duration.ofMillis(shardInfo.getConnectionTimeout())); + setReadTimeout(Duration.ofMillis(shardInfo.getSoTimeout())); + } + } } diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/DefaultLettuceClientConfiguration.java b/src/main/java/org/springframework/data/redis/connection/lettuce/DefaultLettuceClientConfiguration.java new file mode 100644 index 000000000..993c4d813 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/DefaultLettuceClientConfiguration.java @@ -0,0 +1,107 @@ +/* + * Copyright 2017 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 + * + * 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 org.springframework.data.redis.connection.lettuce; + +import io.lettuce.core.ClientOptions; +import io.lettuce.core.resource.ClientResources; + +import java.time.Duration; +import java.util.Optional; + +/** + * Default implementation of {@literal LettuceClientConfiguration}. + * + * @author Mark Paluch + * @since 2.0 + */ +class DefaultLettuceClientConfiguration implements LettuceClientConfiguration { + + private final boolean useSsl; + private final boolean verifyPeer; + private final boolean startTls; + private final Optional clientResources; + private final Optional clientOptions; + private final Duration timeout; + private final Duration shutdownTimeout; + + DefaultLettuceClientConfiguration(boolean useSsl, boolean verifyPeer, boolean startTls, + ClientResources clientResources, ClientOptions clientOptions, Duration timeout, Duration shutdownTimeout) { + + this.useSsl = useSsl; + this.verifyPeer = verifyPeer; + this.startTls = startTls; + this.clientResources = Optional.ofNullable(clientResources); + this.clientOptions = Optional.ofNullable(clientOptions); + this.timeout = timeout; + this.shutdownTimeout = shutdownTimeout; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration#useSsl() + */ + @Override + public boolean useSsl() { + return useSsl; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration#isVerifyPeer() + */ + @Override + public boolean isVerifyPeer() { + return verifyPeer; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration#isStartTls() + */ + @Override + public boolean isStartTls() { + return startTls; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration#getClientResources() + */ + @Override + public Optional getClientResources() { + return clientResources; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration#getClientOptions() + */ + @Override + public Optional getClientOptions() { + return clientOptions; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration#getTimeout() + */ + @Override + public Duration getTimeout() { + return timeout; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration#getShutdownTimeout() + */ + @Override + public Duration getShutdownTimeout() { + return shutdownTimeout; + } +} diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClientConfiguration.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClientConfiguration.java new file mode 100644 index 000000000..c0533cea3 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClientConfiguration.java @@ -0,0 +1,339 @@ +/* + * Copyright 2017 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 + * + * 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 org.springframework.data.redis.connection.lettuce; + +import io.lettuce.core.ClientOptions; +import io.lettuce.core.RedisURI; +import io.lettuce.core.resource.ClientResources; + +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import org.springframework.util.Assert; + +/** + * Redis client configuration for lettuce. This configuration provides optional configuration elements such as + * {@link ClientResources} and {@link ClientOptions} specific to Lettuce client features. + *

+ * Providing optional elements allows a more specific configuration of the client: + *

    + *
  • Whether to use SSL
  • + *
  • Whether to verify peers using SSL
  • + *
  • Whether to use StartTLS
  • + *
  • Optional {@link ClientResources}
  • + *
  • Optional {@link ClientOptions}
  • + *
  • Client {@link Duration timeout}
  • + *
  • Shutdown {@link Duration timeout}
  • + *
+ * + * @author Mark Paluch + * @since 2.0 + * @see org.springframework.data.redis.connection.RedisStandaloneConfiguration + * @see org.springframework.data.redis.connection.RedisSentinelConfiguration + * @see org.springframework.data.redis.connection.RedisClusterConfiguration + */ +public interface LettuceClientConfiguration { + + /** + * @return {@literal true} to use SSL, {@literal false} to use unencrypted connections. + */ + boolean useSsl(); + + /** + * @return {@literal true} to verify peers when using {@link #useSsl() SSL}. + */ + boolean isVerifyPeer(); + + /** + * @return {@literal true} to use Start TLS ({@code true} if the first write request shouldn't be encrypted). + */ + boolean isStartTls(); + + /** + * @return the optional {@link ClientResources}. + */ + Optional getClientResources(); + + /** + * @return the optional {@link io.lettuce.core.ClientOptions}. + */ + Optional getClientOptions(); + + /** + * @return the timeout. + */ + Duration getTimeout(); + + /** + * @return the shutdown timeout used to close the client. + * @see io.lettuce.core.AbstractRedisClient#shutdown(long, long, TimeUnit) + */ + Duration getShutdownTimeout(); + + /** + * Creates a new {@link LettuceClientConfigurationBuilder} to build {@link LettuceClientConfiguration} to be used with + * the Lettuce client. + * + * @return a new {@link LettuceClientConfigurationBuilder} to build {@link LettuceClientConfiguration}. + */ + static LettuceClientConfigurationBuilder builder() { + return new DefaultLettuceClientConfigurationBuilder(); + } + + /** + * Creates an empty {@link LettuceClientConfiguration}. + * + * @return an empty {@link LettuceClientConfiguration}. + */ + static LettuceClientConfiguration create() { + return builder().build(); + } + + /** + * Builder for {@link LettuceClientConfiguration}. + */ + interface LettuceClientConfigurationBuilder { + + /** + * Enable SSL connections. + * + * @return {@link LettuceSslClientConfigurationBuilder}. + */ + LettuceSslClientConfigurationBuilder useSsl(); + + /** + * Use plaintext connections instead of SSL. + * + * @return {@link LettuceClientConfigurationBuilder}. + */ + LettuceClientConfigurationBuilder usePlaintext(); + + /** + * Configure {@link ClientResources}. + * + * @param clientResources must not be {@literal null}. + * @return {@literal this} builder. + */ + LettuceClientConfigurationBuilder clientResources(ClientResources clientResources); + + /** + * Configure {@link ClientOptions}. + * + * @param clientOptions must not be {@literal null}. + * @return {@literal this} builder. + * @see ClientOptions + * @see io.lettuce.core.cluster.ClusterClientOptions + */ + LettuceClientConfigurationBuilder clientOptions(ClientOptions clientOptions); + + /** + * Configure a timeout. + * + * @param timeout must not be {@literal null}. + * @return {@literal this} builder. + */ + LettuceClientConfigurationBuilder timeout(Duration timeout); + + /** + * Configure a shutdown timeout. + * + * @param shutdownTimeout must not be {@literal null}. + * @return {@literal this} builder. + */ + LettuceClientConfigurationBuilder shutdownTimeout(Duration shutdownTimeout); + + /** + * Build the {@link LettuceClientConfiguration} with the configuration applied from this builder. + * + * @return a new {@link LettuceClientConfiguration} object. + */ + LettuceClientConfiguration build(); + } + + /** + * Builder for SSL-related {@link LettuceClientConfiguration}. + */ + interface LettuceSslClientConfigurationBuilder { + + /** + * Enable peer verification. + * + * @return {@literal this} builder. + */ + LettuceSslClientConfigurationBuilder verifyPeer(); + + /** + * Enable/disable peer verification. + * + * @param verifyPeer {@literal true} to enable peer verification, {@literal false} to skip peer verification. + * @return {@literal this} builder. + */ + LettuceSslClientConfigurationBuilder verifyPeer(boolean verifyPeer); + + /** + * Enable Start TLS to send the first bytes unencrypted. + * + * @return {@literal this} builder. + */ + LettuceSslClientConfigurationBuilder startTls(); + + /** + * Return to {@link LettuceClientConfigurationBuilder}. + * + * @return {@link LettuceClientConfigurationBuilder}. + */ + LettuceClientConfigurationBuilder and(); + + /** + * Build the {@link LettuceClientConfiguration} with the configuration applied from this builder. + * + * @return a new {@link LettuceClientConfiguration} object. + */ + LettuceClientConfiguration build(); + } + + /** + * Default {@link LettuceClientConfigurationBuilder} implementation to build an immutable + * {@link LettuceClientConfiguration}. + */ + class DefaultLettuceClientConfigurationBuilder + implements LettuceClientConfigurationBuilder, LettuceSslClientConfigurationBuilder { + + private boolean useSsl; + private boolean verifyPeer = true; + private boolean startTls; + private ClientResources clientResources; + private ClientOptions clientOptions; + private Duration timeout = Duration.ofSeconds(RedisURI.DEFAULT_TIMEOUT); + private Duration shutdownTimeout = Duration.ofSeconds(2); + + private DefaultLettuceClientConfigurationBuilder() {} + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration.LettuceClientConfigurationBuilder#useSsl() + */ + @Override + public LettuceSslClientConfigurationBuilder useSsl() { + + this.useSsl = true; + return this; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration.LettuceClientConfigurationBuilder#usePlaintext() + */ + @Override + public LettuceClientConfigurationBuilder usePlaintext() { + + this.useSsl = false; + return this; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration.LettuceSslClientConfigurationBuilder#verifyPeer() + */ + @Override + public LettuceSslClientConfigurationBuilder verifyPeer() { + return verifyPeer(true); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration.LettuceSslClientConfigurationBuilder#verifyPeer(boolean) + */ + @Override + public LettuceSslClientConfigurationBuilder verifyPeer(boolean verifyPeer) { + + this.verifyPeer = verifyPeer; + return this; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration.LettuceSslClientConfigurationBuilder#startTls() + */ + @Override + public LettuceSslClientConfigurationBuilder startTls() { + + this.startTls = true; + return this; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration.LettuceSslClientConfigurationBuilder#and() + */ + @Override + public LettuceClientConfigurationBuilder and() { + return this; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration.LettuceClientConfigurationBuilder#clientResources(io.lettuce.core.resource.ClientResources) + */ + @Override + public LettuceClientConfigurationBuilder clientResources(ClientResources clientResources) { + + Assert.notNull(clientResources, "ClientResources must not be null!"); + + this.clientResources = clientResources; + return this; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration.LettuceClientConfigurationBuilder#clientOptions(io.lettuce.core.ClientOptions) + */ + @Override + public LettuceClientConfigurationBuilder clientOptions(ClientOptions clientOptions) { + + Assert.notNull(clientOptions, "ClientOptions must not be null!"); + + this.clientOptions = clientOptions; + return this; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration.LettuceClientConfigurationBuilder#timeout(java.time.Duration) + */ + @Override + public LettuceClientConfigurationBuilder timeout(Duration timeout) { + + Assert.notNull(timeout, "Duration must not be null!"); + + this.timeout = timeout; + return this; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration.LettuceClientConfigurationBuilder#shutdownTimeout(java.time.Duration) + */ + @Override + public LettuceClientConfigurationBuilder shutdownTimeout(Duration shutdownTimeout) { + + Assert.notNull(timeout, "Duration must not be null!"); + + this.shutdownTimeout = shutdownTimeout; + return this; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration.LettuceClientConfigurationBuilder#build() + */ + @Override + public LettuceClientConfiguration build() { + return new DefaultLettuceClientConfiguration(useSsl, verifyPeer, startTls, clientResources, clientOptions, + timeout, shutdownTimeout); + } + } +} diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java index 516df39f6..3f2217228 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java @@ -16,15 +16,19 @@ package org.springframework.data.redis.connection.lettuce; import io.lettuce.core.AbstractRedisClient; +import io.lettuce.core.ClientOptions; import io.lettuce.core.RedisClient; import io.lettuce.core.RedisException; import io.lettuce.core.RedisURI; import io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.cluster.ClusterClientOptions; import io.lettuce.core.cluster.RedisClusterClient; import io.lettuce.core.resource.ClientResources; +import java.time.Duration; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.concurrent.TimeUnit; import org.apache.commons.logging.Log; @@ -37,16 +41,7 @@ import org.springframework.dao.InvalidDataAccessResourceUsageException; import org.springframework.data.redis.ExceptionTranslationStrategy; import org.springframework.data.redis.PassThroughExceptionTranslationStrategy; import org.springframework.data.redis.RedisConnectionFailureException; -import org.springframework.data.redis.connection.ClusterCommandExecutor; -import org.springframework.data.redis.connection.Pool; -import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory; -import org.springframework.data.redis.connection.RedisClusterConfiguration; -import org.springframework.data.redis.connection.RedisClusterConnection; -import org.springframework.data.redis.connection.RedisConnection; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.connection.RedisNode; -import org.springframework.data.redis.connection.RedisSentinelConfiguration; -import org.springframework.data.redis.connection.RedisSentinelConnection; +import org.springframework.data.redis.connection.*; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -62,6 +57,14 @@ import org.springframework.util.StringUtils; * a {@link Pool} to pool dedicated connections. If shareNativeConnection is true, the pool will be used to select a * connection for blocking and tx operations only, which should not share a connection. If native connection sharing is * disabled, the selected connection will be used for all operations. + *

+ * {@link LettuceConnectionFactory} should be configured using an environmental configuration and the + * {@link LettuceConnectionFactory client configuration}. Lettuce supports the following environmental configurations: + *

    + *
  • {@link RedisStandaloneConfiguration}
  • + *
  • {@link RedisSentinelConfiguration}
  • + *
  • {@link RedisClusterConfiguration}
  • + *
* * @author Costin Leau * @author Jennifer Hickey @@ -79,67 +82,128 @@ public class LettuceConnectionFactory LettuceConverters.exceptionConverter()); private final Log log = LogFactory.getLog(getClass()); + private final LettuceClientConfiguration clientConfiguration; - private String hostName = "localhost"; - private int port = 6379; private AbstractRedisClient client; - private long timeout = TimeUnit.MILLISECONDS.convert(60, TimeUnit.SECONDS); - private long shutdownTimeout = TimeUnit.MILLISECONDS.convert(2, TimeUnit.SECONDS); private boolean validateConnection = false; private boolean shareNativeConnection = true; private StatefulRedisConnection connection; private LettucePool pool; - private int dbIndex = 0; /** Synchronization monitor for the shared Connection */ private final Object connectionMonitor = new Object(); - private String password; private boolean convertPipelineAndTxResults = true; + private RedisStandaloneConfiguration standaloneConfig = new RedisStandaloneConfiguration("localhost", 6379); private RedisSentinelConfiguration sentinelConfiguration; private RedisClusterConfiguration clusterConfiguration; private ClusterCommandExecutor clusterCommandExecutor; - private ClientResources clientResources; - private boolean useSsl = false; - private boolean verifyPeer = true; - private boolean startTls = false; /** - * Constructs a new LettuceConnectionFactory instance with default settings. + * Constructs a new {@link LettuceConnectionFactory} instance with default settings. */ - public LettuceConnectionFactory() {} + public LettuceConnectionFactory() { + this(new MutableLettuceClientConfiguration()); + } /** - * Constructs a new LettuceConnectionFactory instance with default settings. + * Constructs a new {@link LettuceConnectionFactory} instance given {@link LettuceClientConfiguration}. + * + * @param clientConfig must not be {@literal null} + * @since 2.0 + */ + private LettuceConnectionFactory(LettuceClientConfiguration clientConfig) { + + Assert.notNull(clientConfig, "LettuceClientConfiguration must not be null!"); + + this.clientConfiguration = clientConfig; + } + + /** + * Constructs a new {@link LettuceConnectionFactory} instance with default settings. */ public LettuceConnectionFactory(String host, int port) { - this.hostName = host; - this.port = port; + this(new RedisStandaloneConfiguration(host, port), new MutableLettuceClientConfiguration()); } /** * Constructs a new {@link LettuceConnectionFactory} instance using the given {@link RedisSentinelConfiguration} * - * @param sentinelConfiguration + * @param sentinelConfiguration must not be {@literal null}. * @since 1.6 */ public LettuceConnectionFactory(RedisSentinelConfiguration sentinelConfiguration) { - this.sentinelConfiguration = sentinelConfiguration; + this(sentinelConfiguration, new MutableLettuceClientConfiguration()); } /** * Constructs a new {@link LettuceConnectionFactory} instance using the given {@link RedisClusterConfiguration} * applied to create a {@link RedisClusterClient}. * - * @param clusterConfig + * @param clusterConfiguration must not be {@literal null}. * @since 1.7 */ - public LettuceConnectionFactory(RedisClusterConfiguration clusterConfig) { - this.clusterConfiguration = clusterConfig; + public LettuceConnectionFactory(RedisClusterConfiguration clusterConfiguration) { + this(clusterConfiguration, new MutableLettuceClientConfiguration()); } public LettuceConnectionFactory(LettucePool pool) { + this(new MutableLettuceClientConfiguration()); this.pool = pool; } + /** + * Constructs a new {@link LettuceConnectionFactory} instance using the given {@link RedisStandaloneConfiguration} and + * {@link LettuceClientConfiguration}. + * + * @param standaloneConfig must not be {@literal null}. + * @param clientConfig must not be {@literal null}. + * @since 2.0 + */ + public LettuceConnectionFactory(RedisStandaloneConfiguration standaloneConfig, + LettuceClientConfiguration clientConfig) { + + this(clientConfig); + + Assert.notNull(standaloneConfig, "RedisStandaloneConfiguration must not be null!"); + + this.standaloneConfig = standaloneConfig; + } + + /** + * Constructs a new {@link LettuceConnectionFactory} instance using the given {@link RedisSentinelConfiguration} and + * {@link LettuceClientConfiguration}. + * + * @param sentinelConfiguration must not be {@literal null}. + * @param clientConfig must not be {@literal null}. + * @since 2.0 + */ + public LettuceConnectionFactory(RedisSentinelConfiguration sentinelConfiguration, + LettuceClientConfiguration clientConfig) { + + this(clientConfig); + + Assert.notNull(sentinelConfiguration, "RedisSentinelConfiguration must not be null!"); + + this.sentinelConfiguration = sentinelConfiguration; + } + + /** + * Constructs a new {@link LettuceConnectionFactory} instance using the given {@link RedisClusterConfiguration} and + * {@link LettuceClientConfiguration}. + * + * @param clusterConfiguration must not be {@literal null}. + * @param clientConfig must not be {@literal null}. + * @since 2.0 + */ + public LettuceConnectionFactory(RedisClusterConfiguration clusterConfiguration, + LettuceClientConfiguration clientConfig) { + + this(clientConfig); + + Assert.notNull(clusterConfiguration, "RedisClusterConfiguration must not be null!"); + + this.clusterConfiguration = clusterConfiguration; + } + /* * (non-Javadoc) * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() @@ -157,7 +221,8 @@ public class LettuceConnectionFactory resetConnection(); try { - client.shutdown(shutdownTimeout, shutdownTimeout, TimeUnit.MILLISECONDS); + Duration timeout = clientConfiguration.getShutdownTimeout(); + client.shutdown(timeout.toMillis(), timeout.toMillis(), TimeUnit.MILLISECONDS); } catch (Exception e) { if (log.isWarnEnabled()) { @@ -186,7 +251,8 @@ public class LettuceConnectionFactory return getClusterConnection(); } - LettuceConnection connection = new LettuceConnection(getSharedConnection(), timeout, client, pool, dbIndex); + LettuceConnection connection = new LettuceConnection(getSharedConnection(), getTimeout(), client, pool, + getDatabase()); connection.setConvertPipelineAndTxResults(convertPipelineAndTxResults); return connection; } @@ -283,16 +349,18 @@ public class LettuceConnectionFactory * @return the host. */ public String getHostName() { - return hostName; + return standaloneConfig.getHostName(); } /** - * Sets the host. + * Sets the hostname. * - * @param host the host to set. + * @param hostName the hostname to set. + * @deprecated since 2.0, configure the hostname using {@link RedisStandaloneConfiguration}. */ - public void setHostName(String host) { - this.hostName = host; + @Deprecated + public void setHostName(String hostName) { + standaloneConfig.setHostName(hostName); } /** @@ -301,16 +369,18 @@ public class LettuceConnectionFactory * @return the port. */ public int getPort() { - return port; + return standaloneConfig.getPort(); } /** * Sets the port. * * @param port the port to set. + * @deprecated since 2.0, configure the port using {@link RedisStandaloneConfiguration}. */ + @Deprecated public void setPort(int port) { - this.port = port; + standaloneConfig.setPort(port); } /** @@ -319,25 +389,19 @@ public class LettuceConnectionFactory * @return connection timeout. */ public long getTimeout() { - return timeout; + return getClientTimeout(); } /** * Sets the connection timeout (in milliseconds). * - * @param timeout connection timeout. + * @param timeout the timeout. + * @deprecated since 2.0, configure the timeout using {@link LettuceClientConfiguration}. + * @throws IllegalStateException if {@link LettuceClientConfiguration} is immutable. */ + @Deprecated public void setTimeout(long timeout) { - this.timeout = timeout; - } - - /** - * Sets to use SSL connection. - * - * @param useSsl {@literal true} to use SSL. - */ - public void setUseSsl(boolean useSsl) { - this.useSsl = useSsl; + getMutableConfiguration().setTimeout(Duration.ofMillis(timeout)); } /** @@ -346,25 +410,40 @@ public class LettuceConnectionFactory * @return use of SSL. */ public boolean isUseSsl() { - return useSsl; + return clientConfiguration.useSsl(); + } + + /** + * Sets to use SSL connection. + * + * @param useSsl {@literal true} to use SSL. + * @deprecated since 2.0, configure SSL usage using {@link LettuceClientConfiguration}. + * @throws IllegalStateException if {@link LettuceClientConfiguration} is immutable. + */ + @Deprecated + public void setUseSsl(boolean useSsl) { + getMutableConfiguration().setUseSsl(useSsl); + } + + /** + * Returns whether to verify certificate validity/hostname check when SSL is used. + * + * @return whether to verify peers when using SSL. + */ + public boolean isVerifyPeer() { + return clientConfiguration.isVerifyPeer(); } /** * Sets to use verify certificate validity/hostname check when SSL is used. * * @param verifyPeer {@literal false} not to verify hostname. + * @deprecated since 2.0, configure peer verification using {@link LettuceClientConfiguration}. + * @throws IllegalStateException if {@link LettuceClientConfiguration} is immutable. */ + @Deprecated public void setVerifyPeer(boolean verifyPeer) { - this.verifyPeer = verifyPeer; - } - - /** - * Returns whether to verify certificate validity/hostname check when SSL is used. - * - * @return verify peers when using SSL. - */ - public boolean isVerifyPeer() { - return verifyPeer; + getMutableConfiguration().setVerifyPeer(verifyPeer); } /** @@ -373,16 +452,19 @@ public class LettuceConnectionFactory * @return use of StartTLS. */ public boolean isStartTls() { - return startTls; + return clientConfiguration.isStartTls(); } /** * Sets to issue StartTLS. * * @param startTls {@literal true} to issue StartTLS. + * @deprecated since 2.0, configure StartTLS using {@link LettuceClientConfiguration}. + * @throws IllegalStateException if {@link LettuceClientConfiguration} is immutable. */ + @Deprecated public void setStartTls(boolean startTls) { - this.startTls = startTls; + getMutableConfiguration().setStartTls(startTls); } /** @@ -436,7 +518,12 @@ public class LettuceConnectionFactory * @return the database index. */ public int getDatabase() { - return dbIndex; + + if (isRedisSentinelAware()) { + return sentinelConfiguration.getDatabase(); + } + + return standaloneConfig.getDatabase(); } /** @@ -445,8 +532,15 @@ public class LettuceConnectionFactory * @param index database index */ public void setDatabase(int index) { + Assert.isTrue(index >= 0, "invalid DB index (a positive index required)"); - this.dbIndex = index; + + if (isRedisSentinelAware()) { + sentinelConfiguration.setDatabase(index); + return; + } + + standaloneConfig.setDatabase(index); } /** @@ -455,16 +549,39 @@ public class LettuceConnectionFactory * @return password for authentication. */ public String getPassword() { - return password; + + if (isRedisSentinelAware()) { + return sentinelConfiguration.getPassword(); + } + + if (isClusterAware()) { + return clusterConfiguration.getPassword(); + } + + return standaloneConfig.getPassword(); } /** * Sets the password used for authenticating with the Redis server. * * @param password the password to set + * @deprecated since 2.0, configure the password using {@link RedisStandaloneConfiguration}, + * {@link RedisSentinelConfiguration} or {@link RedisClusterConfiguration}. */ + @Deprecated public void setPassword(String password) { - this.password = password; + + if (isRedisSentinelAware()) { + sentinelConfiguration.setPassword(password); + return; + } + + if (isClusterAware()) { + clusterConfiguration.setPassword(password); + return; + } + + standaloneConfig.setPassword(password); } /** @@ -474,7 +591,7 @@ public class LettuceConnectionFactory * @since 1.6 */ public long getShutdownTimeout() { - return shutdownTimeout; + return clientConfiguration.getShutdownTimeout().toMillis(); } /** @@ -482,9 +599,12 @@ public class LettuceConnectionFactory * * @param shutdownTimeout the shutdown timeout. * @since 1.6 + * @deprecated since 2.0, configure the shutdown timeout using {@link LettuceClientConfiguration}. + * @throws IllegalStateException if {@link LettuceClientConfiguration} is immutable. */ + @Deprecated public void setShutdownTimeout(long shutdownTimeout) { - this.shutdownTimeout = shutdownTimeout; + getMutableConfiguration().setShutdownTimeout(Duration.ofMillis(shutdownTimeout)); } /** @@ -494,7 +614,7 @@ public class LettuceConnectionFactory * @since 1.7 */ public ClientResources getClientResources() { - return clientResources; + return clientConfiguration.getClientResources().orElse(null); } /** @@ -503,9 +623,44 @@ public class LettuceConnectionFactory * * @param clientResources can be {@literal null}. * @since 1.7 + * @deprecated since 2.0, configure {@link ClientResources} using {@link LettuceClientConfiguration}. + * @throws IllegalStateException if {@link LettuceClientConfiguration} is immutable. */ + @Deprecated public void setClientResources(ClientResources clientResources) { - this.clientResources = clientResources; + getMutableConfiguration().setClientResources(clientResources); + } + + /** + * @return the {@link LettuceClientConfiguration}. + * @since 2.0 + */ + public LettuceClientConfiguration getClientConfiguration() { + return clientConfiguration; + } + + /** + * @return the {@link RedisStandaloneConfiguration}. + * @since 2.0 + */ + public RedisStandaloneConfiguration getStandaloneConfiguration() { + return standaloneConfig; + } + + /** + * @return the {@link RedisStandaloneConfiguration}, may be {@literal null}. + * @since 2.0 + */ + public RedisSentinelConfiguration getSentinelConfiguration() { + return sentinelConfiguration; + } + + /** + * @return the {@link RedisClusterConfiguration}, may be {@literal null}. + * @since 2.0 + */ + public RedisClusterConfiguration getClusterConfiguration() { + return clusterConfiguration; } /** @@ -568,8 +723,8 @@ public class LettuceConnectionFactory StatefulRedisConnection connection = null; if (client instanceof RedisClient) { connection = ((RedisClient) client).connect(LettuceConnection.CODEC); - if (dbIndex > 0) { - connection.sync().select(dbIndex); + if (getDatabase() > 0) { + connection.sync().select(getDatabase()); } } else { connection = null; @@ -585,11 +740,12 @@ public class LettuceConnectionFactory if (isRedisSentinelAware()) { RedisURI redisURI = getSentinelRedisURI(); - if (clientResources == null) { - return RedisClient.create(redisURI); - } + RedisClient redisClient = clientConfiguration.getClientResources() // + .map(clientResources -> RedisClient.create(clientResources, redisURI)) // + .orElseGet(() -> RedisClient.create(redisURI)); - return RedisClient.create(clientResources, redisURI); + clientConfiguration.getClientOptions().ifPresent(redisClient::setOptions); + return redisClient; } if (isClusterAware()) { @@ -599,13 +755,18 @@ public class LettuceConnectionFactory initialUris.add(createRedisURIAndApplySettings(node.getHost(), node.getPort())); } - RedisClusterClient clusterClient = clientResources != null - ? RedisClusterClient.create(clientResources, initialUris) : RedisClusterClient.create(initialUris); + RedisClusterClient clusterClient = clientConfiguration.getClientResources() // + .map(clientResources -> RedisClusterClient.create(clientResources, initialUris)) // + .orElseGet(() -> RedisClusterClient.create(initialUris)); this.clusterCommandExecutor = new ClusterCommandExecutor( new LettuceClusterConnection.LettuceClusterTopologyProvider(clusterClient), new LettuceClusterConnection.LettuceClusterNodeResourceProvider(clusterClient), EXCEPTION_TRANSLATION); + clientConfiguration.getClientOptions() // + .filter(clientOptions -> clientOptions instanceof ClusterClientOptions) // + .ifPresent(clientOptions -> clusterClient.setOptions((ClusterClientOptions) clientOptions)); + return clusterClient; } @@ -613,16 +774,20 @@ public class LettuceConnectionFactory return pool.getClient(); } - RedisURI uri = createRedisURIAndApplySettings(hostName, port); - return clientResources != null ? RedisClient.create(clientResources, uri) : RedisClient.create(uri); + RedisURI uri = createRedisURIAndApplySettings(getHostName(), getPort()); + RedisClient redisClient = clientConfiguration.getClientResources() // + .map(clientResources -> RedisClient.create(clientResources, uri)) // + .orElseGet(() -> RedisClient.create(uri)); + clientConfiguration.getClientOptions().ifPresent(redisClient::setOptions); + return redisClient; } private RedisURI getSentinelRedisURI() { RedisURI redisUri = LettuceConverters.sentinelConfigurationToRedisURI(sentinelConfiguration); - if (StringUtils.hasText(password)) { - redisUri.setPassword(password); + if (StringUtils.hasText(getPassword())) { + redisUri.setPassword(getPassword()); } return redisUri; @@ -631,14 +796,14 @@ public class LettuceConnectionFactory private RedisURI createRedisURIAndApplySettings(String host, int port) { RedisURI.Builder builder = RedisURI.Builder.redis(host, port); - if (StringUtils.hasText(password)) { - builder.withPassword(password); + if (StringUtils.hasText(getPassword())) { + builder.withPassword(getPassword()); } - builder.withSsl(useSsl); - builder.withVerifyPeer(verifyPeer); - builder.withStartTls(startTls); - builder.withTimeout(timeout, TimeUnit.MILLISECONDS); + builder.withSsl(clientConfiguration.useSsl()); + builder.withVerifyPeer(clientConfiguration.isVerifyPeer()); + builder.withStartTls(clientConfiguration.isStartTls()); + builder.withTimeout(clientConfiguration.getTimeout().toMillis(), TimeUnit.MILLISECONDS); return builder.build(); } @@ -651,4 +816,107 @@ public class LettuceConnectionFactory } return new LettuceSentinelConnection(((RedisClient) client).connectSentinel()); } + + private MutableLettuceClientConfiguration getMutableConfiguration() { + + Assert.state(clientConfiguration instanceof MutableLettuceClientConfiguration, + () -> String.format("Client configuration must be instance of MutableLettuceClientConfiguration but is %s", + ClassUtils.getShortName(clientConfiguration.getClass()))); + + return (MutableLettuceClientConfiguration) clientConfiguration; + } + + private long getClientTimeout() { + return clientConfiguration.getTimeout().toMillis(); + } + + static class MutableLettuceClientConfiguration implements LettuceClientConfiguration { + + private boolean useSsl; + private boolean verifyPeer = true; + private boolean startTls; + private ClientResources clientResources; + private Duration timeout = Duration.ofSeconds(RedisURI.DEFAULT_TIMEOUT); + private Duration shutdownTimeout = Duration.ofSeconds(RedisURI.DEFAULT_TIMEOUT); + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration#useSsl() + */ + @Override + public boolean useSsl() { + return useSsl; + } + + public void setUseSsl(boolean useSsl) { + this.useSsl = useSsl; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration#isVerifyPeer() + */ + @Override + public boolean isVerifyPeer() { + return verifyPeer; + } + + public void setVerifyPeer(boolean verifyPeer) { + this.verifyPeer = verifyPeer; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration#isStartTls() + */ + @Override + public boolean isStartTls() { + return startTls; + } + + public void setStartTls(boolean startTls) { + this.startTls = startTls; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration#getClientResources() + */ + @Override + public Optional getClientResources() { + return Optional.ofNullable(clientResources); + } + + public void setClientResources(ClientResources clientResources) { + this.clientResources = clientResources; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration#getClientOptions() + */ + @Override + public Optional getClientOptions() { + return Optional.empty(); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration#getTimeout() + */ + @Override + public Duration getTimeout() { + return timeout; + } + + public void setTimeout(Duration timeout) { + this.timeout = timeout; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration#getShutdownTimeout() + */ + @Override + public Duration getShutdownTimeout() { + return shutdownTimeout; + } + + public void setShutdownTimeout(Duration shutdownTimeout) { + this.shutdownTimeout = shutdownTimeout; + } + } } diff --git a/src/test/java/org/springframework/data/redis/ConnectionFactoryTracker.java b/src/test/java/org/springframework/data/redis/ConnectionFactoryTracker.java index b0dfbf83a..d0b03b3bf 100644 --- a/src/test/java/org/springframework/data/redis/ConnectionFactoryTracker.java +++ b/src/test/java/org/springframework/data/redis/ConnectionFactoryTracker.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2013 the original author or authors. + * Copyright 2011-2017 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. @@ -15,7 +15,9 @@ */ package org.springframework.data.redis; +import java.util.ArrayList; import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; import org.springframework.beans.factory.DisposableBean; @@ -26,6 +28,7 @@ import org.springframework.data.redis.connection.RedisConnectionFactory; * factory during setup and then call {@link #cleanUp()} through the @AfterClass method. * * @author Costin Leau + * @author Mark Paluch */ public abstract class ConnectionFactoryTracker { @@ -41,12 +44,14 @@ public abstract class ConnectionFactoryTracker { public static void cleanUp() { if (connFactories != null) { - for (Object connectionFactory : connFactories) { + List copy = new ArrayList<>(connFactories); + for (Object connectionFactory : copy) { try { if (connectionFactory instanceof DisposableBean) { ((DisposableBean) connectionFactory).destroy(); // System.out.println("Succesfully cleaned up factory " + connectionFactory); } + connFactories.remove(connectionFactory); } catch (Exception ex) { System.err.println("Cannot clean factory " + connectionFactory + ex); } diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisClientConfigurationUnitTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisClientConfigurationUnitTests.java new file mode 100644 index 000000000..4bac761c4 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisClientConfigurationUnitTests.java @@ -0,0 +1,105 @@ +/* + * Copyright 2017 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 + * + * 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 org.springframework.data.redis.connection.jedis; + +import static org.assertj.core.api.Assertions.*; + +import redis.clients.jedis.JedisPoolConfig; + +import java.security.NoSuchAlgorithmException; +import java.time.Duration; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocketFactory; + +import org.junit.Test; + +/** + * Unit tests for {@link JedisClientConfiguration}. + * + * @author Mark Paluch + */ +public class JedisClientConfigurationUnitTests { + + @Test // DATAREDIS-574 + public void shouldCreateEmptyConfiguration() { + + JedisClientConfiguration configuration = JedisClientConfiguration.create(); + + assertThat(configuration.getClientName()).isEmpty(); + assertThat(configuration.getConnectTimeout()).isEqualTo(Duration.ofSeconds(2)); + assertThat(configuration.getReadTimeout()).isEqualTo(Duration.ofSeconds(2)); + assertThat(configuration.getHostnameVerifier()).isEmpty(); + assertThat(configuration.getPoolConfig()).isPresent(); + assertThat(configuration.getSslParameters()).isEmpty(); + assertThat(configuration.getSslSocketFactory()).isEmpty(); + } + + @Test // DATAREDIS-574 + public void shouldConfigureAllProperties() throws NoSuchAlgorithmException { + + SSLParameters sslParameters = new SSLParameters(); + SSLContext context = SSLContext.getDefault(); + SSLSocketFactory socketFactory = context.getSocketFactory(); + JedisPoolConfig poolConfig = new JedisPoolConfig(); + + JedisClientConfiguration configuration = JedisClientConfiguration.builder().useSsl() // + .hostnameVerifier(MyHostnameVerifier.INSTANCE) // + .sslParameters(sslParameters) // + .sslSocketFactory(socketFactory).and() // + .clientName("my-client") // + .connectTimeout(Duration.ofMinutes(10)) // + .readTimeout(Duration.ofHours(5)) // + .usePooling().poolConfig(poolConfig) // + .build(); + + assertThat(configuration.useSsl()).isTrue(); + assertThat(configuration.getHostnameVerifier()).contains(MyHostnameVerifier.INSTANCE); + assertThat(configuration.getSslParameters()).contains(sslParameters); + assertThat(configuration.getSslSocketFactory()).contains(socketFactory); + + assertThat(configuration.getClientName()).contains("my-client"); + assertThat(configuration.getConnectTimeout()).isEqualTo(Duration.ofMinutes(10)); + assertThat(configuration.getReadTimeout()).isEqualTo(Duration.ofHours(5)); + + assertThat(configuration.getPoolConfig()).contains(poolConfig); + } + + @Test // DATAREDIS-574 + public void shouldAllowsConfigurationOverrides() { + + JedisClientConfiguration configuration = JedisClientConfiguration.builder().useSsl() // + .and().usePlaintext() // + .usePooling().and().useUnpooledConnections() // + .build(); + + assertThat(configuration.useSsl()).isFalse(); + assertThat(configuration.usePooling()).isFalse(); + } + + enum MyHostnameVerifier implements HostnameVerifier { + + INSTANCE; + + @Override + public boolean verify(String s, SSLSession sslSession) { + return false; + } + } +} diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryIntegrationTests.java new file mode 100644 index 000000000..8b390072a --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryIntegrationTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2017 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 + * + * 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 org.springframework.data.redis.connection.jedis; + +import static org.hamcrest.core.IsEqual.*; +import static org.junit.Assert.*; + +import redis.clients.jedis.JedisShardInfo; + +import org.junit.After; +import org.junit.Test; +import org.springframework.data.redis.SettingsUtils; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; + +/** + * Integration tests for {@link JedisConnectionFactory}. + * + * @author Mark Paluch + */ +public class JedisConnectionFactoryIntegrationTests { + + private JedisConnectionFactory factory; + + @After + public void tearDown() { + + if (factory != null) { + factory.destroy(); + } + } + + @Test // DATAREDIS-574 + public void shardInfoShouldOverrideFactorySettings() { + + factory = new JedisConnectionFactory(new JedisShardInfo(SettingsUtils.getHost(), SettingsUtils.getPort())); + factory.setUsePool(false); + factory.setPassword("foo"); + factory.setHostName("bar"); + factory.setPort(1234); + factory.afterPropertiesSet(); + + assertThat(factory.getConnection().ping(), equalTo("PONG")); + } + + @Test // DATAREDIS-574 + public void shouldInitiaizeWithStandaloneConfiguration() { + + factory = new JedisConnectionFactory( + new RedisStandaloneConfiguration(SettingsUtils.getHost(), SettingsUtils.getPort()), + JedisClientConfiguration.create()); + factory.afterPropertiesSet(); + + assertThat(factory.getConnection().ping(), equalTo("PONG")); + } +} diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactorySentinelIntegrationTests.java similarity index 72% rename from src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryTests.java rename to src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactorySentinelIntegrationTests.java index ece335c32..98e3fb97d 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactorySentinelIntegrationTests.java @@ -19,17 +19,19 @@ import static org.hamcrest.core.IsEqual.*; import static org.junit.Assert.*; import org.junit.After; -import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.springframework.data.redis.connection.RedisSentinelConfiguration; import org.springframework.data.redis.test.util.RedisSentinelRule; /** + * Sentinel integration tests for {@link JedisConnectionFactory}. + * * @author Christoph Strobl * @author Fu Jian + * @author Mark Paluch */ -public class JedisConnectionFactoryTests { +public class JedisConnectionFactorySentinelIntegrationTests { private static final RedisSentinelConfiguration SENTINEL_CONFIG = new RedisSentinelConfiguration().master("mymaster") .sentinel("127.0.0.1", 26379).sentinel("127.0.0.1", 26380); @@ -37,25 +39,43 @@ public class JedisConnectionFactoryTests { public @Rule RedisSentinelRule sentinelRule = RedisSentinelRule.forConfig(SENTINEL_CONFIG).oneActive(); - @Before - public void setUp() { - factory = new JedisConnectionFactory(SENTINEL_CONFIG); - factory.setClientName("clientName"); - factory.afterPropertiesSet(); - } - @After public void tearDown() { - factory.destroy(); + + if (factory != null) { + factory.destroy(); + } + } + + @Test // DATAREDIS-574 + public void shouldInitiaizeWithSentinelConfiguration() { + + JedisClientConfiguration clientConfiguration = JedisClientConfiguration.builder() // + .clientName("clientName") // + .build(); + + factory = new JedisConnectionFactory(SENTINEL_CONFIG, clientConfiguration); + factory.afterPropertiesSet(); + + assertThat(factory.getConnection().getClientName(), equalTo("clientName")); } @Test // DATAREDIS-324 public void shouldSendCommandCorrectlyViaConnectionFactoryUsingSentinel() { + + factory = new JedisConnectionFactory(SENTINEL_CONFIG); + factory.afterPropertiesSet(); + assertThat(factory.getConnection().ping(), equalTo("PONG")); } @Test // DATAREDIS-552 public void getClientNameShouldEqualWithFactorySetting() { + + factory = new JedisConnectionFactory(SENTINEL_CONFIG); + factory.setClientName("clientName"); + factory.afterPropertiesSet(); + assertThat(factory.getConnection().getClientName(), equalTo("clientName")); } } diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryUnitTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryUnitTests.java index 427bf64e7..b4fb20140 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryUnitTests.java @@ -15,31 +15,44 @@ */ package org.springframework.data.redis.connection.jedis; +import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; +import redis.clients.jedis.JedisCluster; +import redis.clients.jedis.JedisPoolConfig; +import sun.net.www.protocol.https.DefaultHostnameVerifier; + import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.time.Duration; +import java.time.temporal.ChronoUnit; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSocketFactory; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import org.junit.Test; import org.springframework.data.redis.connection.RedisClusterConfiguration; import org.springframework.data.redis.connection.RedisSentinelConfiguration; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.test.util.ReflectionTestUtils; -import redis.clients.jedis.JedisCluster; -import redis.clients.jedis.JedisPoolConfig; - /** + * Unit tests for {@link JedisConnectionFactory}. + * * @author Christoph Strobl + * @author Mark Paluch */ public class JedisConnectionFactoryUnitTests { private JedisConnectionFactory connectionFactory; - private static final RedisSentinelConfiguration SINGLE_SENTINEL_CONFIG = new RedisSentinelConfiguration().master( - "mymaster").sentinel("127.0.0.1", 26379); + private static final RedisSentinelConfiguration SINGLE_SENTINEL_CONFIG = new RedisSentinelConfiguration() + .master("mymaster").sentinel("127.0.0.1", 26379); - private static final RedisClusterConfiguration CLUSTER_CONFIG = new RedisClusterConfiguration().clusterNode( - "127.0.0.1", 6379).clusterNode("127.0.0.1", 6380); + private static final RedisClusterConfiguration CLUSTER_CONFIG = new RedisClusterConfiguration() + .clusterNode("127.0.0.1", 6379).clusterNode("127.0.0.1", 6380); @Test // DATAREDIS-324 public void shouldInitSentinelPoolWhenSentinelConfigPresent() { @@ -73,7 +86,7 @@ public class JedisConnectionFactoryUnitTests { } @Test // DATAREDIS-315 - public void shouldClostClusterCorrectlyOnFactoryDestruction() throws IOException { + public void shouldCloseClusterCorrectlyOnFactoryDestruction() throws IOException { JedisCluster clusterMock = mock(JedisCluster.class); JedisConnectionFactory factory = new JedisConnectionFactory(); @@ -84,6 +97,196 @@ public class JedisConnectionFactoryUnitTests { verify(clusterMock, times(1)).close(); } + @Test // DATAREDIS-574 + public void shouldReadStandalonePassword() { + + RedisStandaloneConfiguration envConfig = new RedisStandaloneConfiguration(); + envConfig.setPassword("foo"); + + connectionFactory = new JedisConnectionFactory(envConfig, JedisClientConfiguration.create()); + + assertThat(connectionFactory.getPassword()).isEqualTo("foo"); + } + + @Test // DATAREDIS-574 + public void shouldWriteStandalonePassword() { + + RedisStandaloneConfiguration envConfig = new RedisStandaloneConfiguration(); + envConfig.setPassword("foo"); + + connectionFactory = new JedisConnectionFactory(envConfig, JedisClientConfiguration.create()); + connectionFactory.setPassword("bar"); + + assertThat(connectionFactory.getPassword()).isEqualTo("bar"); + assertThat(envConfig.getPassword()).isEqualTo("bar"); + } + + @Test // DATAREDIS-574 + public void shouldReadSentinelPassword() { + + RedisSentinelConfiguration envConfig = new RedisSentinelConfiguration(); + envConfig.setPassword("foo"); + + connectionFactory = new JedisConnectionFactory(envConfig, JedisClientConfiguration.create()); + + assertThat(connectionFactory.getPassword()).isEqualTo("foo"); + } + + @Test // DATAREDIS-574 + public void shouldWriteSentinelPassword() { + + RedisSentinelConfiguration envConfig = new RedisSentinelConfiguration(); + envConfig.setPassword("foo"); + + connectionFactory = new JedisConnectionFactory(envConfig, JedisClientConfiguration.create()); + connectionFactory.setPassword("bar"); + + assertThat(connectionFactory.getPassword()).isEqualTo("bar"); + assertThat(envConfig.getPassword()).isEqualTo("bar"); + } + + @Test // DATAREDIS-574 + public void shouldReadClusterPassword() { + + RedisClusterConfiguration envConfig = new RedisClusterConfiguration(); + envConfig.setPassword("foo"); + + connectionFactory = new JedisConnectionFactory(envConfig, JedisClientConfiguration.create()); + + assertThat(connectionFactory.getPassword()).isEqualTo("foo"); + } + + @Test // DATAREDIS-574 + public void shouldWriteClusterPassword() { + + RedisClusterConfiguration envConfig = new RedisClusterConfiguration(); + envConfig.setPassword("foo"); + + connectionFactory = new JedisConnectionFactory(envConfig, JedisClientConfiguration.create()); + connectionFactory.setPassword("bar"); + + assertThat(connectionFactory.getPassword()).isEqualTo("bar"); + assertThat(envConfig.getPassword()).isEqualTo("bar"); + } + + @Test // DATAREDIS-574 + public void shouldReadStandaloneDatabaseIndex() { + + RedisStandaloneConfiguration envConfig = new RedisStandaloneConfiguration(); + envConfig.setDatabase(2); + + connectionFactory = new JedisConnectionFactory(envConfig, JedisClientConfiguration.create()); + + assertThat(connectionFactory.getDatabase()).isEqualTo(2); + } + + @Test // DATAREDIS-574 + public void shouldWriteStandaloneDatabaseIndex() { + + RedisStandaloneConfiguration envConfig = new RedisStandaloneConfiguration(); + envConfig.setDatabase(2); + + connectionFactory = new JedisConnectionFactory(envConfig, JedisClientConfiguration.create()); + connectionFactory.setDatabase(3); + + assertThat(connectionFactory.getDatabase()).isEqualTo(3); + assertThat(envConfig.getDatabase()).isEqualTo(3); + } + + @Test // DATAREDIS-574 + public void shouldReadSentinelDatabaseIndex() { + + RedisSentinelConfiguration envConfig = new RedisSentinelConfiguration(); + envConfig.setDatabase(2); + + connectionFactory = new JedisConnectionFactory(envConfig, JedisClientConfiguration.create()); + + assertThat(connectionFactory.getDatabase()).isEqualTo(2); + } + + @Test // DATAREDIS-574 + public void shouldWriteSentinelDatabaseIndex() { + + RedisSentinelConfiguration envConfig = new RedisSentinelConfiguration(); + envConfig.setDatabase(2); + + connectionFactory = new JedisConnectionFactory(envConfig, JedisClientConfiguration.create()); + connectionFactory.setDatabase(3); + + assertThat(connectionFactory.getDatabase()).isEqualTo(3); + assertThat(envConfig.getDatabase()).isEqualTo(3); + } + + @Test // DATAREDIS-574 + public void shouldApplyClientConfiguration() throws NoSuchAlgorithmException { + + SSLParameters sslParameters = new SSLParameters(); + SSLContext context = SSLContext.getDefault(); + SSLSocketFactory socketFactory = context.getSocketFactory(); + JedisPoolConfig poolConfig = new JedisPoolConfig(); + + JedisClientConfiguration configuration = JedisClientConfiguration.builder().useSsl() // + .hostnameVerifier(new DefaultHostnameVerifier()) // + .sslParameters(sslParameters) // + .sslSocketFactory(socketFactory).and() // + .clientName("my-client") // + .connectTimeout(Duration.of(10, ChronoUnit.MINUTES)) // + .readTimeout(Duration.of(5, ChronoUnit.DAYS)) // + .usePooling().poolConfig(poolConfig) // + .build(); + + connectionFactory = new JedisConnectionFactory(new RedisStandaloneConfiguration(), configuration); + + assertThat(connectionFactory.getClientConfiguration()).isSameAs(configuration); + assertThat(connectionFactory.isUseSsl()).isTrue(); + assertThat(connectionFactory.getClientName()).isEqualTo("my-client"); + assertThat(connectionFactory.getTimeout()).isEqualTo((int) Duration.of(5, ChronoUnit.DAYS).toMillis()); + assertThat(connectionFactory.getUsePool()).isTrue(); + assertThat(connectionFactory.getPoolConfig()).isSameAs(poolConfig); + } + + @Test // DATAREDIS-574 + public void shouldReturnStandaloneConfiguration() { + + RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(); + connectionFactory = new JedisConnectionFactory(configuration, JedisClientConfiguration.create()); + + assertThat(connectionFactory.getStandaloneConfiguration()).isSameAs(configuration); + assertThat(connectionFactory.getSentinelConfiguration()).isNull(); + assertThat(connectionFactory.getClusterConfiguration()).isNull(); + } + + @Test // DATAREDIS-574 + public void shouldReturnSentinelConfiguration() { + + RedisSentinelConfiguration configuration = new RedisSentinelConfiguration(); + connectionFactory = new JedisConnectionFactory(configuration, JedisClientConfiguration.create()); + + assertThat(connectionFactory.getStandaloneConfiguration()).isNotNull(); + assertThat(connectionFactory.getSentinelConfiguration()).isSameAs(configuration); + assertThat(connectionFactory.getClusterConfiguration()).isNull(); + } + + @Test // DATAREDIS-574 + public void shouldReturnClusterConfiguration() { + + RedisClusterConfiguration configuration = new RedisClusterConfiguration(); + connectionFactory = new JedisConnectionFactory(configuration, JedisClientConfiguration.create()); + + assertThat(connectionFactory.getStandaloneConfiguration()).isNotNull(); + assertThat(connectionFactory.getSentinelConfiguration()).isNull(); + assertThat(connectionFactory.getClusterConfiguration()).isSameAs(configuration); + } + + @Test(expected = IllegalStateException.class) // DATAREDIS-574 + public void shouldDenyChangesToImmutableClientConfiguration() throws NoSuchAlgorithmException { + + connectionFactory = new JedisConnectionFactory(new RedisStandaloneConfiguration(), + JedisClientConfiguration.create()); + + connectionFactory.setClientName("foo"); + } + private JedisConnectionFactory initSpyedConnectionFactory(RedisSentinelConfiguration sentinelConfig, JedisPoolConfig poolConfig) { diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClientConfigurationUnitTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClientConfigurationUnitTests.java new file mode 100644 index 000000000..17191980c --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClientConfigurationUnitTests.java @@ -0,0 +1,82 @@ +/* + * Copyright 2017 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 + * + * 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 org.springframework.data.redis.connection.lettuce; + +import static org.assertj.core.api.Assertions.*; + +import io.lettuce.core.ClientOptions; +import io.lettuce.core.resource.ClientResources; + +import java.time.Duration; + +import org.junit.Test; + +/** + * Unit tests for {@link LettuceClientConfiguration}. + * + * @author Mark Paluch + */ +public class LettuceClientConfigurationUnitTests { + + @Test // DATAREDIS-574 + public void shouldCreateEmptyConfiguration() { + + LettuceClientConfiguration configuration = LettuceClientConfiguration.create(); + + assertThat(configuration.useSsl()).isFalse(); + assertThat(configuration.isVerifyPeer()).isTrue(); + assertThat(configuration.isStartTls()).isFalse(); + assertThat(configuration.getClientOptions()).isEmpty(); + assertThat(configuration.getClientResources()).isEmpty(); + assertThat(configuration.getTimeout()).isEqualTo(Duration.ofSeconds(60)); + assertThat(configuration.getShutdownTimeout()).isEqualTo(Duration.ofSeconds(2)); + } + + @Test // DATAREDIS-574 + public void shouldConfigureAllProperties() { + + ClientOptions clientOptions = ClientOptions.create(); + ClientResources sharedClientResources = LettuceTestClientResources.getSharedClientResources(); + + LettuceClientConfiguration configuration = LettuceClientConfiguration.builder() // + .useSsl() // + .verifyPeer(false) // + .startTls().and() // + .clientOptions(clientOptions) // + .clientResources(sharedClientResources) // + .timeout(Duration.ofMinutes(5)) // + .shutdownTimeout(Duration.ofHours(2)) // + .build(); + + assertThat(configuration.useSsl()).isTrue(); + assertThat(configuration.isVerifyPeer()).isFalse(); + assertThat(configuration.isStartTls()).isTrue(); + assertThat(configuration.getClientOptions()).contains(clientOptions); + assertThat(configuration.getClientResources()).contains(sharedClientResources); + assertThat(configuration.getTimeout()).isEqualTo(Duration.ofMinutes(5)); + assertThat(configuration.getShutdownTimeout()).isEqualTo(Duration.ofHours(2)); + } + + @Test // DATAREDIS-574 + public void shouldAllowsConfigurationOverrides() { + + LettuceClientConfiguration configuration = LettuceClientConfiguration.builder() // + .useSsl().and().usePlaintext() // + .build(); + + assertThat(configuration.useSsl()).isFalse(); + } +} diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryUnitTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryUnitTests.java index a57db4708..5baf91c10 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryUnitTests.java @@ -18,16 +18,21 @@ package org.springframework.data.redis.connection.lettuce; import static org.hamcrest.core.Is.*; import static org.hamcrest.core.IsEqual.*; import static org.hamcrest.core.IsInstanceOf.*; +import static org.hamcrest.core.IsNull.*; import static org.junit.Assert.*; import static org.springframework.data.redis.connection.ClusterTestVariables.*; import static org.springframework.data.redis.connection.lettuce.LettuceTestClientResources.*; import static org.springframework.test.util.ReflectionTestUtils.*; import io.lettuce.core.AbstractRedisClient; +import io.lettuce.core.ClientOptions; import io.lettuce.core.RedisClient; import io.lettuce.core.RedisURI; import io.lettuce.core.cluster.RedisClusterClient; +import io.lettuce.core.resource.ClientResources; +import java.security.NoSuchAlgorithmException; +import java.time.Duration; import java.util.Collections; import java.util.concurrent.TimeUnit; @@ -37,8 +42,11 @@ import org.junit.Test; import org.springframework.data.redis.ConnectionFactoryTracker; import org.springframework.data.redis.connection.RedisClusterConfiguration; import org.springframework.data.redis.connection.RedisSentinelConfiguration; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; /** + * Unit tests for {@link LettuceConnectionFactory}. + * * @author Christoph Strobl * @author Mark Paluch * @author Balázs Németh @@ -275,4 +283,208 @@ public class LettuceConnectionFactoryUnitTests { assertThat(uri.isVerifyPeer(), is(true)); } } + + @Test // DATAREDIS-574 + public void shouldReadStandalonePassword() { + + RedisStandaloneConfiguration envConfig = new RedisStandaloneConfiguration(); + envConfig.setPassword("foo"); + + LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(envConfig, + LettuceClientConfiguration.create()); + + assertThat(connectionFactory.getPassword(), is(equalTo("foo"))); + } + + @Test // DATAREDIS-574 + public void shouldWriteStandalonePassword() { + + RedisStandaloneConfiguration envConfig = new RedisStandaloneConfiguration(); + envConfig.setPassword("foo"); + + LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(envConfig, + LettuceClientConfiguration.create()); + connectionFactory.setPassword("bar"); + + assertThat(connectionFactory.getPassword(), is(equalTo("bar"))); + assertThat(envConfig.getPassword(), is(equalTo("bar"))); + } + + @Test // DATAREDIS-574 + public void shouldReadSentinelPassword() { + + RedisSentinelConfiguration envConfig = new RedisSentinelConfiguration(); + envConfig.setPassword("foo"); + + LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(envConfig, + LettuceClientConfiguration.create()); + + assertThat(connectionFactory.getPassword(), is(equalTo("foo"))); + } + + @Test // DATAREDIS-574 + public void shouldWriteSentinelPassword() { + + RedisSentinelConfiguration envConfig = new RedisSentinelConfiguration(); + envConfig.setPassword("foo"); + + LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(envConfig, + LettuceClientConfiguration.create()); + connectionFactory.setPassword("bar"); + + assertThat(connectionFactory.getPassword(), is(equalTo("bar"))); + assertThat(envConfig.getPassword(), is(equalTo("bar"))); + } + + @Test // DATAREDIS-574 + public void shouldReadClusterPassword() { + + RedisClusterConfiguration envConfig = new RedisClusterConfiguration(); + envConfig.setPassword("foo"); + + LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(envConfig, + LettuceClientConfiguration.create()); + + assertThat(connectionFactory.getPassword(), is(equalTo("foo"))); + } + + @Test // DATAREDIS-574 + public void shouldWriteClusterPassword() { + + RedisClusterConfiguration envConfig = new RedisClusterConfiguration(); + envConfig.setPassword("foo"); + + LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(envConfig, + LettuceClientConfiguration.create()); + connectionFactory.setPassword("bar"); + + assertThat(connectionFactory.getPassword(), is(equalTo("bar"))); + assertThat(envConfig.getPassword(), is(equalTo("bar"))); + } + + @Test // DATAREDIS-574 + public void shouldReadStandaloneDatabaseIndex() { + + RedisStandaloneConfiguration envConfig = new RedisStandaloneConfiguration(); + envConfig.setDatabase(2); + + LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(envConfig, + LettuceClientConfiguration.create()); + + assertThat(connectionFactory.getDatabase(), is(2)); + } + + @Test // DATAREDIS-574 + public void shouldWriteStandaloneDatabaseIndex() { + + RedisStandaloneConfiguration envConfig = new RedisStandaloneConfiguration(); + envConfig.setDatabase(2); + + LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(envConfig, + LettuceClientConfiguration.create()); + connectionFactory.setDatabase(3); + + assertThat(connectionFactory.getDatabase(), is(3)); + assertThat(envConfig.getDatabase(), is(3)); + } + + @Test // DATAREDIS-574 + public void shouldReadSentinelDatabaseIndex() { + + RedisSentinelConfiguration envConfig = new RedisSentinelConfiguration(); + envConfig.setDatabase(2); + + LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(envConfig, + LettuceClientConfiguration.create()); + + assertThat(connectionFactory.getDatabase(), is(2)); + } + + @Test // DATAREDIS-574 + public void shouldWriteSentinelDatabaseIndex() { + + RedisSentinelConfiguration envConfig = new RedisSentinelConfiguration(); + envConfig.setDatabase(2); + + LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(envConfig, + LettuceClientConfiguration.create()); + connectionFactory.setDatabase(3); + + assertThat(connectionFactory.getDatabase(), is(3)); + assertThat(envConfig.getDatabase(), is(3)); + } + + @Test // DATAREDIS-574 + public void shouldApplyClientConfiguration() { + + ClientOptions clientOptions = ClientOptions.create(); + ClientResources sharedClientResources = LettuceTestClientResources.getSharedClientResources(); + + LettuceClientConfiguration configuration = LettuceClientConfiguration.builder() // + .useSsl() // + .verifyPeer(false) // + .startTls().and() // + .clientOptions(clientOptions) // + .clientResources(sharedClientResources) // + .timeout(Duration.ofMinutes(5)) // + .shutdownTimeout(Duration.ofHours(2)) // + .build(); + + LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(new RedisStandaloneConfiguration(), + configuration); + + assertThat(connectionFactory.getClientConfiguration(), is(configuration)); + + assertThat(connectionFactory.isUseSsl(), is(true)); + assertThat(connectionFactory.isVerifyPeer(), is(false)); + assertThat(connectionFactory.isStartTls(), is(true)); + assertThat(connectionFactory.getClientResources(), is(sharedClientResources)); + assertThat(connectionFactory.getTimeout(), is(Duration.ofMinutes(5).toMillis())); + assertThat(connectionFactory.getShutdownTimeout(), is(Duration.ofHours(2).toMillis())); + } + + @Test // DATAREDIS-574 + public void shouldReturnStandaloneConfiguration() { + + RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(); + LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(configuration, + LettuceClientConfiguration.create()); + + assertThat(connectionFactory.getStandaloneConfiguration(), is(configuration)); + assertThat(connectionFactory.getSentinelConfiguration(), is(nullValue())); + assertThat(connectionFactory.getClusterConfiguration(), is(nullValue())); + } + + @Test // DATAREDIS-574 + public void shouldReturnSentinelConfiguration() { + + RedisSentinelConfiguration configuration = new RedisSentinelConfiguration(); + LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(configuration, + LettuceClientConfiguration.create()); + + assertThat(connectionFactory.getStandaloneConfiguration(), is(notNullValue())); + assertThat(connectionFactory.getSentinelConfiguration(), is(configuration)); + assertThat(connectionFactory.getClusterConfiguration(), is(nullValue())); + } + + @Test // DATAREDIS-574 + public void shouldReturnClusterConfiguration() { + + RedisClusterConfiguration configuration = new RedisClusterConfiguration(); + LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(configuration, + LettuceClientConfiguration.create()); + + assertThat(connectionFactory.getStandaloneConfiguration(), is(notNullValue())); + assertThat(connectionFactory.getSentinelConfiguration(), is(nullValue())); + assertThat(connectionFactory.getClusterConfiguration(), is(configuration)); + } + + @Test(expected = IllegalStateException.class) // DATAREDIS-574 + public void shouldDenyChangesToImmutableClientConfiguration() throws NoSuchAlgorithmException { + + LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(new RedisStandaloneConfiguration(), + LettuceClientConfiguration.create()); + + connectionFactory.setUseSsl(false); + } } diff --git a/src/test/java/org/springframework/data/redis/core/AbstractOperationsTestParams.java b/src/test/java/org/springframework/data/redis/core/AbstractOperationsTestParams.java index 0810090c5..ebf1ff1ad 100644 --- a/src/test/java/org/springframework/data/redis/core/AbstractOperationsTestParams.java +++ b/src/test/java/org/springframework/data/redis/core/AbstractOperationsTestParams.java @@ -26,6 +26,7 @@ import org.springframework.data.redis.PersonObjectFactory; import org.springframework.data.redis.RawObjectFactory; import org.springframework.data.redis.SettingsUtils; import org.springframework.data.redis.StringObjectFactory; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.GenericToStringSerializer; @@ -46,6 +47,9 @@ abstract public class AbstractOperationsTestParams { // DATAREDIS-241 public static Collection testParams() { + RedisStandaloneConfiguration standaloneConfiguration = new RedisStandaloneConfiguration(SettingsUtils.getHost(), + SettingsUtils.getPort()); + ObjectFactory stringFactory = new StringObjectFactory(); ObjectFactory longFactory = new LongObjectFactory(); ObjectFactory doubleFactory = new DoubleObjectFactory(); @@ -60,9 +64,7 @@ abstract public class AbstractOperationsTestParams { throw new RuntimeException("Cannot init XStream", ex); } - JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(); - jedisConnectionFactory.setPort(SettingsUtils.getPort()); - jedisConnectionFactory.setHostName(SettingsUtils.getHost()); + JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(standaloneConfiguration); jedisConnectionFactory.afterPropertiesSet(); RedisTemplate stringTemplate = new StringRedisTemplate(); diff --git a/src/test/java/org/springframework/data/redis/core/ReactiveOperationsTestParams.java b/src/test/java/org/springframework/data/redis/core/ReactiveOperationsTestParams.java index 0b142f772..9d8dafd7f 100644 --- a/src/test/java/org/springframework/data/redis/core/ReactiveOperationsTestParams.java +++ b/src/test/java/org/springframework/data/redis/core/ReactiveOperationsTestParams.java @@ -18,6 +18,7 @@ package org.springframework.data.redis.core; import static org.springframework.data.redis.connection.ClusterTestVariables.*; import java.nio.ByteBuffer; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -35,6 +36,8 @@ import org.springframework.data.redis.SettingsUtils; import org.springframework.data.redis.StringObjectFactory; import org.springframework.data.redis.connection.RedisClusterConfiguration; import org.springframework.data.redis.connection.RedisClusterNode; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.connection.lettuce.LettuceTestClientResources; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; @@ -57,6 +60,14 @@ abstract public class ReactiveOperationsTestParams { public static Collection testParams() { + LettuceClientConfiguration clientConfiguration = LettuceClientConfiguration.builder() // + .shutdownTimeout(Duration.ZERO) // + .clientResources(LettuceTestClientResources.getSharedClientResources()) // + .build(); + + RedisStandaloneConfiguration standaloneConfiguration = new RedisStandaloneConfiguration(SettingsUtils.getHost(), + SettingsUtils.getPort()); + ObjectFactory stringFactory = new StringObjectFactory(); ObjectFactory clusterKeyStringFactory = new PrefixStringObjectFactory("{u1}.", stringFactory); ObjectFactory longFactory = new LongObjectFactory(); @@ -72,10 +83,8 @@ abstract public class ReactiveOperationsTestParams { throw new RuntimeException("Cannot init XStream", ex); } - LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(); - lettuceConnectionFactory.setClientResources(LettuceTestClientResources.getSharedClientResources()); - lettuceConnectionFactory.setPort(SettingsUtils.getPort()); - lettuceConnectionFactory.setHostName(SettingsUtils.getHost()); + LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(standaloneConfiguration, + clientConfiguration); lettuceConnectionFactory.afterPropertiesSet(); JdkSerializationRedisSerializer jdkSerializationRedisSerializer = new JdkSerializationRedisSerializer(); @@ -137,9 +146,8 @@ abstract public class ReactiveOperationsTestParams { ReactiveRedisTemplate clusterStringTemplate = null; - LettuceConnectionFactory lettuceClusterConnectionFactory = new LettuceConnectionFactory(clusterConfiguration); - lettuceClusterConnectionFactory.setPort(SettingsUtils.getPort()); - lettuceClusterConnectionFactory.setHostName(SettingsUtils.getHost()); + LettuceConnectionFactory lettuceClusterConnectionFactory = new LettuceConnectionFactory(clusterConfiguration, + clientConfiguration); lettuceClusterConnectionFactory.afterPropertiesSet(); clusterStringTemplate = new ReactiveRedisTemplate<>(lettuceClusterConnectionFactory,