diff --git a/src/main/java/org/springframework/data/redis/listener/KeyExpirationEventMessageListener.java b/src/main/java/org/springframework/data/redis/listener/KeyExpirationEventMessageListener.java index 61039da973..4b370010df 100644 --- a/src/main/java/org/springframework/data/redis/listener/KeyExpirationEventMessageListener.java +++ b/src/main/java/org/springframework/data/redis/listener/KeyExpirationEventMessageListener.java @@ -25,24 +25,36 @@ /** * {@link MessageListener} publishing {@link RedisKeyExpiredEvent}s via {@link ApplicationEventPublisher} by listening * to Redis keyspace notifications for key expirations. + *
+ * For development-time convenience the {@link #setKeyspaceNotificationsConfigParameter(String)} is set to + * {@literal "Ex"}, by default. However, it is strongly recommended that users specifically set + * {@literal notify-keyspace-events} to the appropriate value on the Redis server, in {@literal redis.conf}. + *
+ * Any Redis server configuration coming from your Spring (Data Redis) application only occurs during Spring container + * initialization, and is not persisted across Redis server restarts. * * @author Christoph Strobl + * @author John Blum * @since 1.7 */ public class KeyExpirationEventMessageListener extends KeyspaceEventMessageListener implements ApplicationEventPublisherAware { + private static final String EXPIRED_KEY_EVENTS = "Ex"; + private static final Topic KEYEVENT_EXPIRED_TOPIC = new PatternTopic("__keyevent@*__:expired"); private @Nullable ApplicationEventPublisher publisher; /** - * Creates new {@link MessageListener} for {@code __keyevent@*__:expired} messages. + * Creates new {@link MessageListener} for {@code __keyevent@*__:expired} messages and configures notification on + * expired keys ({@literal Ex}). * * @param listenerContainer must not be {@literal null}. */ public KeyExpirationEventMessageListener(RedisMessageListenerContainer listenerContainer) { super(listenerContainer); + setKeyspaceNotificationsConfigParameter(EXPIRED_KEY_EVENTS); } @Override diff --git a/src/main/java/org/springframework/data/redis/listener/KeyspaceEventMessageListener.java b/src/main/java/org/springframework/data/redis/listener/KeyspaceEventMessageListener.java index eba222f5fd..59b013bd0c 100644 --- a/src/main/java/org/springframework/data/redis/listener/KeyspaceEventMessageListener.java +++ b/src/main/java/org/springframework/data/redis/listener/KeyspaceEventMessageListener.java @@ -17,11 +17,14 @@ import java.util.Properties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.data.redis.connection.Message; import org.springframework.data.redis.connection.MessageListener; import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -29,89 +32,168 @@ /** * Base {@link MessageListener} implementation for listening to Redis keyspace notifications. + *
+ * By default, this {@link MessageListener} does not listen for, or notify on, any keyspace events. You must explicitly + * set the {@link #setKeyspaceNotificationsConfigParameter(String)} to a valid {@literal redis.conf}, + * {@literal notify-keyspace-events} value (for example: {@literal EA}) to enable keyspace event notifications + * from your Redis server. + *
+ * Any configuration set in the Redis server take precedence. Therefore, if the Redis server already set a value + * for {@literal notify-keyspace-events}, then any {@link #setKeyspaceNotificationsConfigParameter(String)} + * specified on this listener will be ignored. + *
+ * It is recommended that all infrastructure settings, such as {@literal notify-keyspace-events}, be configured on
+ * the Redis server itself. If the Redis server is rebooted, then any keyspace event configuration coming from
+ * the application will be lost when the Redis server is restarted since Redis server configuration is not persistent,
+ * and any configuration coming from your application only occurs during Spring container initialization.
*
* @author Christoph Strobl
* @author Mark Paluch
+ * @author John Blum
* @since 1.7
*/
public abstract class KeyspaceEventMessageListener implements MessageListener, InitializingBean, DisposableBean {
+ static final String NOTIFY_KEYSPACE_EVENTS = "notify-keyspace-events";
+
private static final Topic TOPIC_ALL_KEYEVENTS = new PatternTopic("__keyevent@*");
- private final RedisMessageListenerContainer listenerContainer;
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ private final RedisMessageListenerContainer messageListenerContainer;
- private String keyspaceNotificationsConfigParameter = "EA";
+ private String keyspaceNotificationsConfigParameter = "";
/**
- * Creates new {@link KeyspaceEventMessageListener}.
+ * Creates a new {@link KeyspaceEventMessageListener}.
*
- * @param listenerContainer must not be {@literal null}.
+ * @param messageListenerContainer {@link RedisMessageListenerContainer} in which this listener will be registered;
+ * must not be {@literal null}.
*/
- public KeyspaceEventMessageListener(RedisMessageListenerContainer listenerContainer) {
+ public KeyspaceEventMessageListener(RedisMessageListenerContainer messageListenerContainer) {
- Assert.notNull(listenerContainer, "RedisMessageListenerContainer to run in must not be null");
- this.listenerContainer = listenerContainer;
+ Assert.notNull(messageListenerContainer, "RedisMessageListenerContainer to run in must not be null");
+
+ this.messageListenerContainer = messageListenerContainer;
+ }
+
+ /**
+ * Returns a reference to the configured {@link Logger}.
+ *
+ * @return a reference to the configured {@link Logger}.
+ */
+ protected Logger getLogger() {
+ return this.logger;
+ }
+
+ /**
+ * Returns a configured reference to the {@link RedisMessageListenerContainer} to which this {@link MessageListener}
+ * is registered.
+ *
+ * @return a configured reference to the {@link RedisMessageListenerContainer} to which this {@link MessageListener}
+ * is registered.
+ */
+ protected RedisMessageListenerContainer getMessageListenerContainer() {
+ return this.messageListenerContainer;
}
@Override
public void onMessage(Message message, @Nullable byte[] pattern) {
- if (ObjectUtils.isEmpty(message.getChannel()) || ObjectUtils.isEmpty(message.getBody())) {
- return;
+ if (containsChannelContent(message)) {
+ doHandleMessage(message);
}
+ }
- doHandleMessage(message);
+ // Message must have a channel and body (contain content)
+ private boolean containsChannelContent(Message message) {
+ return !(ObjectUtils.isEmpty(message.getChannel()) || ObjectUtils.isEmpty(message.getBody()));
}
/**
- * Handle the actual message
+ * Handle the actual {@link Message}.
*
- * @param message never {@literal null}.
+ * @param message {@link Message} to process; never {@literal null}.
*/
protected abstract void doHandleMessage(Message message);
+ @Override
+ public void afterPropertiesSet() throws Exception {
+ init();
+ }
+
/**
- * Initialize the message listener by writing requried redis config for {@literal notify-keyspace-events} and
- * registering the listener within the container.
+ * Initialize this {@link MessageListener} by writing required Redis server config
+ * for {@literal notify-keyspace-events} and registering this {@link MessageListener}
+ * with the {@link RedisMessageListenerContainer}.
*/
public void init() {
- if (StringUtils.hasText(keyspaceNotificationsConfigParameter)) {
+ String keyspaceNotificationsConfigParameter = getKeyspaceNotificationsConfigParameter();
- RedisConnection connection = listenerContainer.getConnectionFactory().getConnection();
+ if (isSet(keyspaceNotificationsConfigParameter)) {
+ configureKeyspaceEventNotifications(keyspaceNotificationsConfigParameter);
+ }
+
+ doRegister(getMessageListenerContainer());
+ }
- try {
+ private boolean isSet(@Nullable String value) {
+ return StringUtils.hasText(value);
+ }
- Properties config = connection.getConfig("notify-keyspace-events");
+ void configureKeyspaceEventNotifications(String keyspaceNotificationsConfigParameter) {
- if (!StringUtils.hasText(config.getProperty("notify-keyspace-events"))) {
- connection.setConfig("notify-keyspace-events", keyspaceNotificationsConfigParameter);
- }
+ RedisConnectionFactory connectionFactory = getMessageListenerContainer().getConnectionFactory();
- } finally {
- connection.close();
+ if (connectionFactory != null) {
+ try (RedisConnection connection = connectionFactory.getConnection()) {
+ if (canChangeNotifyKeyspaceEvents(connection)) {
+ setKeyspaceEventNotifications(connection, keyspaceNotificationsConfigParameter);
+ }
}
}
+ else {
+ if (getLogger().isWarnEnabled()) {
+ getLogger().warn("Unable to configure notification on keyspace events;"
+ + " no RedisConnectionFactory was configured in the RedisMessageListenerContainer");
+ }
+ }
+ }
+
+ private boolean canChangeNotifyKeyspaceEvents(@Nullable RedisConnection connection) {
+
+ if (connection != null) {
+
+ Properties config = connection.serverCommands().getConfig(NOTIFY_KEYSPACE_EVENTS);
+
+ return config == null || !isSet(config.getProperty(NOTIFY_KEYSPACE_EVENTS));
+ }
+
+ return false;
+ }
+
+ void setKeyspaceEventNotifications(RedisConnection connection, String keyspaceNotificationsConfigParameter) {
+ connection.serverCommands().setConfig(NOTIFY_KEYSPACE_EVENTS, keyspaceNotificationsConfigParameter);
+ }
- doRegister(listenerContainer);
+ @Override
+ public void destroy() throws Exception {
+ getMessageListenerContainer().removeMessageListener(this);
}
/**
- * Register instance within the container.
+ * Register instance within the {@link RedisMessageListenerContainer}.
*
* @param container never {@literal null}.
*/
protected void doRegister(RedisMessageListenerContainer container) {
- listenerContainer.addMessageListener(this, TOPIC_ALL_KEYEVENTS);
- }
-
- @Override
- public void destroy() throws Exception {
- listenerContainer.removeMessageListener(this);
+ container.addMessageListener(this, TOPIC_ALL_KEYEVENTS);
}
/**
- * Set the configuration string to use for {@literal notify-keyspace-events}.
+ * Set the {@link String configuration setting} (for example: {@literal EA}) to use
+ * for {@literal notify-keyspace-events}.
*
* @param keyspaceNotificationsConfigParameter can be {@literal null}.
* @since 1.8
@@ -120,8 +202,13 @@ public void setKeyspaceNotificationsConfigParameter(String keyspaceNotifications
this.keyspaceNotificationsConfigParameter = keyspaceNotificationsConfigParameter;
}
- @Override
- public void afterPropertiesSet() throws Exception {
- init();
+ /**
+ * Get the configured {@link String setting} for {@literal notify-keyspace-events}.
+ *
+ * @return the configured {@link String setting} for {@literal notify-keyspace-events}.
+ */
+ @Nullable
+ protected String getKeyspaceNotificationsConfigParameter() {
+ return this.keyspaceNotificationsConfigParameter;
}
}
diff --git a/src/test/java/org/springframework/data/redis/listener/KeyExpirationEventMessageListenerIntegrationTests.java b/src/test/java/org/springframework/data/redis/listener/KeyExpirationEventMessageListenerIntegrationTests.java
index dc4b13e02c..1194c78463 100644
--- a/src/test/java/org/springframework/data/redis/listener/KeyExpirationEventMessageListenerIntegrationTests.java
+++ b/src/test/java/org/springframework/data/redis/listener/KeyExpirationEventMessageListenerIntegrationTests.java
@@ -27,12 +27,10 @@
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
-import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
-import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.extension.JedisConnectionFactoryExtension;
import org.springframework.data.redis.test.extension.RedisStanalone;
@@ -103,7 +101,7 @@ void listenerShouldPublishEventCorrectly() {
@Test // DATAREDIS-425
void listenerShouldNotReactToDeleteEvents() throws InterruptedException {
- byte[] key = ("to-delete:" + UUID.randomUUID().toString()).getBytes();
+ byte[] key = ("to-delete:" + UUID.randomUUID()).getBytes();
try (RedisConnection connection = connectionFactory.getConnection()) {
diff --git a/src/test/java/org/springframework/data/redis/listener/KeyspaceEventMessageListenerUnitTests.java b/src/test/java/org/springframework/data/redis/listener/KeyspaceEventMessageListenerUnitTests.java
new file mode 100644
index 0000000000..3feb48a00d
--- /dev/null
+++ b/src/test/java/org/springframework/data/redis/listener/KeyspaceEventMessageListenerUnitTests.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright 2017-2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.redis.listener;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.withSettings;
+
+import java.util.Properties;
+import java.util.function.Consumer;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.quality.Strictness;
+
+import org.springframework.data.redis.connection.Message;
+import org.springframework.data.redis.connection.RedisConnection;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.RedisServerCommands;
+import org.springframework.lang.Nullable;
+
+/**
+ * Unit tests for {@link KeyspaceEventMessageListener}.
+ *
+ * @author John Blum
+ */
+@ExtendWith(MockitoExtension.class)
+class KeyspaceEventMessageListenerUnitTests {
+
+ @Mock
+ private RedisMessageListenerContainer mockMessageListenerContainer;
+
+ private Message mockMessage(@Nullable String channel, @Nullable String body) {
+
+ Message mockMessage = mock(Message.class, withSettings().strictness(Strictness.LENIENT));
+
+ doReturn(toBytes(body)).when(mockMessage).getBody();
+ doReturn(toBytes(channel)).when(mockMessage).getChannel();
+
+ return mockMessage;
+ }
+
+ @Nullable
+ private byte[] toBytes(@Nullable String value) {
+ return value != null ? value.getBytes() : null;
+ }
+
+ private KeyspaceEventMessageListener newKeyspaceEventMessageListener() {
+ return newKeyspaceEventMessageListener(listener -> { });
+ }
+
+ private KeyspaceEventMessageListener newKeyspaceEventMessageListener(
+ Consumer