diff --git a/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java b/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java index 481d7463f..8d2c5cf4a 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java +++ b/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java @@ -21,12 +21,16 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationListener; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.ConverterNotFoundException; @@ -50,13 +54,14 @@ import org.springframework.data.redis.listener.RedisMessageListenerContainer; import org.springframework.data.redis.util.ByteUtils; import org.springframework.data.util.CloseableIterator; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; /** * Redis specific {@link KeyValueAdapter} implementation. Uses binary codec to read/write data from/to Redis. Objects * are stored in a Redis Hash using the value of {@link RedisHash}, the {@link KeyspaceConfiguration} or just * {@link Class#getName()} as a prefix.
* Example - * + * *
  * 
  * @RedisHash("persons")
@@ -64,8 +69,8 @@ import org.springframework.util.Assert;
  *   @Id String id;
  *   String name;
  * }
- * 
- * 
+ *
+ *
  *         prefix              ID
  *           |                 |
  *           V                 V
@@ -76,29 +81,33 @@ import org.springframework.util.Assert;
  * 4) Rand al'Thor
  * 
  * 
- * + * *
* The {@link KeyValueAdapter} is not intended to store simple types such as {@link String} values. * Please use {@link RedisTemplate} for this purpose. - * + * * @author Christoph Strobl * @author Mark Paluch * @since 1.7 */ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter - implements ApplicationContextAware, ApplicationListener { + implements InitializingBean, ApplicationContextAware, ApplicationListener { private static final Logger LOGGER = LoggerFactory.getLogger(RedisKeyValueAdapter.class); private RedisOperations redisOps; private RedisConverter converter; private RedisMessageListenerContainer messageListenerContainer; - private KeyExpirationEventMessageListener expirationListener; + private AtomicReference expirationListener = new AtomicReference( + null); + private ApplicationEventPublisher eventPublisher; + + private EnableKeyspaceEvents enableKeyspaceEvents = EnableKeyspaceEvents.ON_STARTUP; /** * Creates new {@link RedisKeyValueAdapter} with default {@link RedisMappingContext} and default * {@link CustomConversions}. - * + * * @param redisOps must not be {@literal null}. */ public RedisKeyValueAdapter(RedisOperations redisOps) { @@ -107,7 +116,7 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter /** * Creates new {@link RedisKeyValueAdapter} with default {@link CustomConversions}. - * + * * @param redisOps must not be {@literal null}. * @param mappingContext must not be {@literal null}. */ @@ -117,7 +126,7 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter /** * Creates new {@link RedisKeyValueAdapter}. - * + * * @param redisOps must not be {@literal null}. * @param mappingContext must not be {@literal null}. * @param customConversions can be {@literal null}. @@ -138,12 +147,12 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter converter = mappingConverter; this.redisOps = redisOps; - initKeyExpirationListener(); + intiMessageListenerContainer(); } /** * Creates new {@link RedisKeyValueAdapter} with specific {@link RedisConverter}. - * + * * @param redisOps must not be {@literal null}. * @param mappingContext must not be {@literal null}. */ @@ -156,7 +165,7 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter converter = redisConverter; this.redisOps = redisOps; - initKeyExpirationListener(); + intiMessageListenerContainer(); } /** @@ -175,6 +184,14 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter converter.write(item, rdo); } + if (ObjectUtils.nullSafeEquals(EnableKeyspaceEvents.ON_DEMAND, enableKeyspaceEvents) + && this.expirationListener.get() == null) { + + if (rdo.getTimeToLive() != null && rdo.getTimeToLive().longValue() > 0) { + initKeyExpirationListener(); + } + } + if (rdo.getId() == null) { rdo.setId(converter.getConversionService().convert(id, String.class)); @@ -397,7 +414,7 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter /** * Execute {@link RedisCallback} via underlying {@link RedisOperations}. - * + * * @param callback must not be {@literal null}. * @see RedisOperations#execute(RedisCallback) * @return @@ -408,7 +425,7 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter /** * Get the {@link RedisConverter} in use. - * + * * @return never {@literal null}. */ public RedisConverter getConverter() { @@ -430,7 +447,7 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter /** * Convert given source to binary representation using the underlying {@link ConversionService}. - * + * * @param source * @return * @throws ConverterNotFoundException @@ -444,13 +461,38 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter return converter.getConversionService().convert(source, byte[].class); } + /** + * Configure usage of {@link KeyExpirationEventMessageListener}. + * + * @param enableKeyspaceEvents + * @since 1.8 + */ + public void setEnableKeyspaceEvents(EnableKeyspaceEvents enableKeyspaceEvents) { + this.enableKeyspaceEvents = enableKeyspaceEvents; + } + + /** + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + * @since 1.8 + */ + @Override + public void afterPropertiesSet() { + + if (ObjectUtils.nullSafeEquals(EnableKeyspaceEvents.ON_STARTUP, this.enableKeyspaceEvents)) { + initKeyExpirationListener(); + } + } + /* * (non-Javadoc) * @see org.springframework.beans.factory.DisposableBean#destroy() */ public void destroy() throws Exception { - this.expirationListener.destroy(); + if (this.expirationListener.get() != null) { + this.expirationListener.get().destroy(); + } + this.messageListenerContainer.destroy(); } @@ -490,26 +532,39 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter */ @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.expirationListener.setApplicationEventPublisher(applicationContext); + this.eventPublisher = applicationContext; } - private void initKeyExpirationListener() { + private void intiMessageListenerContainer() { this.messageListenerContainer = new RedisMessageListenerContainer(); messageListenerContainer.setConnectionFactory(((RedisTemplate) redisOps).getConnectionFactory()); messageListenerContainer.afterPropertiesSet(); messageListenerContainer.start(); + } - this.expirationListener = new MappingExpirationListener(this.messageListenerContainer, this.redisOps, - this.converter); - this.expirationListener.init(); + private void initKeyExpirationListener() { + + if (this.expirationListener.get() == null) { + + MappingExpirationListener listener = new MappingExpirationListener(this.messageListenerContainer, this.redisOps, + this.converter); + + if (this.eventPublisher != null) { + listener.setApplicationEventPublisher(this.eventPublisher); + } + + if (this.expirationListener.compareAndSet(null, listener)) { + listener.init(); + } + } } /** * {@link MessageListener} implementation used to capture Redis keypspace notifications. Tries to read a previously * created phantom key {@code keyspace:id:phantom} to provide the expired object as part of the published * {@link RedisKeyExpiredEvent}. - * + * * @author Christoph Strobl * @since 1.7 */ @@ -520,7 +575,7 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter /** * Creates new {@link MappingExpirationListener}. - * + * * @param listenerContainer * @param ops * @param converter @@ -582,4 +637,26 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter } } + /** + * @author Christoph Strobl + * @since 1.8 + */ + public static enum EnableKeyspaceEvents { + + /** + * Initializes the {@link KeyExpirationEventMessageListener} on startup. + */ + ON_STARTUP, + + /** + * Initializes the {@link KeyExpirationEventMessageListener} on first insert having expiration time set. + */ + ON_DEMAND, + + /** + * Turn {@link KeyExpirationEventMessageListener} usage off. No expiration events will be received. + */ + OFF + } + } diff --git a/src/main/java/org/springframework/data/redis/repository/configuration/EnableRedisRepositories.java b/src/main/java/org/springframework/data/redis/repository/configuration/EnableRedisRepositories.java index 329a3d652..128048c43 100644 --- a/src/main/java/org/springframework/data/redis/repository/configuration/EnableRedisRepositories.java +++ b/src/main/java/org/springframework/data/redis/repository/configuration/EnableRedisRepositories.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2016 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. @@ -27,9 +27,11 @@ import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.Import; import org.springframework.data.keyvalue.core.KeyValueOperations; import org.springframework.data.keyvalue.repository.config.QueryCreatorType; +import org.springframework.data.redis.core.RedisKeyValueAdapter.EnableKeyspaceEvents; import org.springframework.data.redis.core.RedisOperations; import org.springframework.data.redis.core.convert.KeyspaceConfiguration; import org.springframework.data.redis.core.index.IndexConfiguration; +import org.springframework.data.redis.listener.KeyExpirationEventMessageListener; import org.springframework.data.redis.repository.query.RedisQueryCreator; import org.springframework.data.redis.repository.support.RedisRepositoryFactoryBean; import org.springframework.data.repository.config.DefaultRepositoryBaseClass; @@ -53,7 +55,8 @@ public @interface EnableRedisRepositories { /** * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation declarations e.g.: - * {@code @EnableRedisRepositories("org.my.pkg")} instead of {@code @EnableRedisRepositories(basePackages="org.my.pkg")}. + * {@code @EnableRedisRepositories("org.my.pkg")} instead of + * {@code @EnableRedisRepositories(basePackages="org.my.pkg")}. */ String[] value() default {}; @@ -154,4 +157,12 @@ public @interface EnableRedisRepositories { */ Class keyspaceConfiguration() default KeyspaceConfiguration.class; + /** + * Configure usage of {@link KeyExpirationEventMessageListener}. + * + * @return + * @since 1.8 + */ + EnableKeyspaceEvents enableKeyspaceEvents() default EnableKeyspaceEvents.ON_DEMAND; + } diff --git a/src/main/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtension.java b/src/main/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtension.java index fd34a64f1..f6ed24137 100644 --- a/src/main/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtension.java +++ b/src/main/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2016 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. @@ -122,6 +122,11 @@ public class RedisRepositoryConfigurationExtension extends KeyValueRepositoryCon new RuntimeBeanReference(REDIS_CONVERTER_BEAN_NAME)); redisKeyValueAdapterDefinition.setConstructorArgumentValues(constructorArgumentValuesForRedisKeyValueAdapter); + + DirectFieldAccessor dfa = new DirectFieldAccessor(configurationSource); + AnnotationAttributes aa = (AnnotationAttributes) dfa.getPropertyValue("attributes"); + redisKeyValueAdapterDefinition.setAttribute("enableKeyspaceEvents", aa.getEnum("enableKeyspaceEvents")); + registerIfNotAlreadyRegistered(redisKeyValueAdapterDefinition, registry, REDIS_ADAPTER_BEAN_NAME, configurationSource); diff --git a/src/test/java/org/springframework/data/redis/core/RedisKeyValueAdapterTests.java b/src/test/java/org/springframework/data/redis/core/RedisKeyValueAdapterTests.java index 8084ee8d5..4c580c2c2 100644 --- a/src/test/java/org/springframework/data/redis/core/RedisKeyValueAdapterTests.java +++ b/src/test/java/org/springframework/data/redis/core/RedisKeyValueAdapterTests.java @@ -69,6 +69,7 @@ public class RedisKeyValueAdapterTests { mappingContext.afterPropertiesSet(); adapter = new RedisKeyValueAdapter(template, mappingContext); + adapter.afterPropertiesSet(); template.execute(new RedisCallback() { diff --git a/src/test/java/org/springframework/data/redis/core/RedisKeyValueAdapterUnitTests.java b/src/test/java/org/springframework/data/redis/core/RedisKeyValueAdapterUnitTests.java index 6783ed65e..19e13e3c0 100644 --- a/src/test/java/org/springframework/data/redis/core/RedisKeyValueAdapterUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/RedisKeyValueAdapterUnitTests.java @@ -16,51 +16,73 @@ package org.springframework.data.redis.core; +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; import static org.mockito.Matchers.*; +import static org.mockito.Matchers.any; import static org.mockito.Mockito.*; +import static org.springframework.test.util.ReflectionTestUtils.*; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashSet; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.annotation.Id; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; +import org.springframework.data.redis.core.RedisKeyValueAdapter.EnableKeyspaceEvents; import org.springframework.data.redis.core.convert.Bucket; +import org.springframework.data.redis.core.convert.KeyspaceConfiguration; +import org.springframework.data.redis.core.convert.MappingConfiguration; import org.springframework.data.redis.core.convert.RedisData; import org.springframework.data.redis.core.convert.SimpleIndexedPropertyValue; +import org.springframework.data.redis.core.index.IndexConfiguration; +import org.springframework.data.redis.core.mapping.RedisMappingContext; +import org.springframework.data.redis.listener.KeyExpirationEventMessageListener; /** * Unit tests for {@link RedisKeyValueAdapter}. * - * @author Mark Paluch * @author Christoph Strobl + * @author Mark Paluch */ @RunWith(MockitoJUnitRunner.class) public class RedisKeyValueAdapterUnitTests { - RedisTemplate redisTemplate; - RedisKeyValueAdapter redisKeyValueAdapter; - + RedisKeyValueAdapter adapter; + RedisTemplate template; + RedisMappingContext context; @Mock JedisConnectionFactory jedisConnectionFactoryMock; @Mock RedisConnection redisConnectionMock; @Before public void setUp() throws Exception { - redisTemplate = new RedisTemplate(); - redisTemplate.setConnectionFactory(jedisConnectionFactoryMock); - redisTemplate.afterPropertiesSet(); + template = new RedisTemplate(); + template.setConnectionFactory(jedisConnectionFactoryMock); + template.afterPropertiesSet(); when(jedisConnectionFactoryMock.getConnection()).thenReturn(redisConnectionMock); when(redisConnectionMock.getConfig("notify-keyspace-events")) .thenReturn(Arrays.asList("notify-keyspace-events", "KEA")); - redisKeyValueAdapter = new RedisKeyValueAdapter(redisTemplate); + context = new RedisMappingContext(new MappingConfiguration(new IndexConfiguration(), new KeyspaceConfiguration())); + context.afterPropertiesSet(); + + adapter = new RedisKeyValueAdapter(template, context); + adapter.afterPropertiesSet(); + } + + @After + public void tearDown() throws Exception { + adapter.destroy(); } /** @@ -69,7 +91,7 @@ public class RedisKeyValueAdapterUnitTests { @Test public void destroyShouldNotDestroyConnectionFactory() throws Exception { - redisKeyValueAdapter.destroy(); + adapter.destroy(); verify(jedisConnectionFactoryMock, never()).destroy(); } @@ -87,7 +109,7 @@ public class RedisKeyValueAdapterUnitTests { .thenReturn(new LinkedHashSet(Arrays.asList("persons:firstname:rand".getBytes()))); when(redisConnectionMock.del((byte[][]) anyVararg())).thenReturn(1L); - redisKeyValueAdapter.put("1", rd, "persons"); + adapter.put("1", rd, "persons"); verify(redisConnectionMock, times(1)).sRem(any(byte[].class), any(byte[].class)); } @@ -105,8 +127,88 @@ public class RedisKeyValueAdapterUnitTests { .thenReturn(new LinkedHashSet(Arrays.asList("persons:firstname:rand".getBytes()))); when(redisConnectionMock.del((byte[][]) anyVararg())).thenReturn(0L); - redisKeyValueAdapter.put("1", rd, "persons"); + adapter.put("1", rd, "persons"); verify(redisConnectionMock, never()).sRem(any(byte[].class), (byte[][]) anyVararg()); } + + /** + * @see DATAREDIS-491 + */ + @Test + public void shouldInitKeyExpirationListenerOnStartup() throws Exception{ + + adapter.destroy(); + + adapter = new RedisKeyValueAdapter(template, context); + adapter.setEnableKeyspaceEvents(EnableKeyspaceEvents.ON_STARTUP); + adapter.afterPropertiesSet(); + + KeyExpirationEventMessageListener listener = ((AtomicReference) getField(adapter, + "expirationListener")).get(); + assertThat(listener, notNullValue()); + } + + /** + * @see DATAREDIS-491 + */ + @Test + public void shouldInitKeyExpirationListenerOnFirstPutWithTtl() throws Exception { + + adapter.destroy(); + + adapter = new RedisKeyValueAdapter(template, context); + adapter.setEnableKeyspaceEvents(EnableKeyspaceEvents.ON_DEMAND); + adapter.afterPropertiesSet(); + + KeyExpirationEventMessageListener listener = ((AtomicReference) getField(adapter, + "expirationListener")).get(); + assertThat(listener, nullValue()); + + adapter.put("should-NOT-start-listener", new WithoutTimeToLive(), "keyspace"); + + listener = ((AtomicReference) getField(adapter, "expirationListener")).get(); + assertThat(listener, nullValue()); + + adapter.put("should-start-listener", new WithTimeToLive(), "keyspace"); + + listener = ((AtomicReference) getField(adapter, "expirationListener")).get(); + assertThat(listener, notNullValue()); + } + + /** + * @see DATAREDIS-491 + */ + @Test + public void shouldNeverInitKeyExpirationListener() throws Exception { + + adapter.destroy(); + + adapter = new RedisKeyValueAdapter(template, context); + adapter.setEnableKeyspaceEvents(EnableKeyspaceEvents.OFF); + adapter.afterPropertiesSet(); + + KeyExpirationEventMessageListener listener = ((AtomicReference) getField(adapter, + "expirationListener")).get(); + assertThat(listener, nullValue()); + + adapter.put("should-NOT-start-listener", new WithoutTimeToLive(), "keyspace"); + + listener = ((AtomicReference) getField(adapter, "expirationListener")).get(); + assertThat(listener, nullValue()); + + adapter.put("should-start-listener", new WithTimeToLive(), "keyspace"); + + listener = ((AtomicReference) getField(adapter, "expirationListener")).get(); + assertThat(listener, nullValue()); + } + + static class WithoutTimeToLive { + @Id String id; + } + + @RedisHash(timeToLive = 10) + static class WithTimeToLive { + @Id String id; + } } diff --git a/src/test/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtensionUnitTests.java b/src/test/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtensionUnitTests.java index 4c81841a8..0f3fc2fd1 100644 --- a/src/test/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtensionUnitTests.java +++ b/src/test/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtensionUnitTests.java @@ -19,8 +19,11 @@ import static org.junit.Assert.*; import java.util.Collection; +import static org.hamcrest.core.IsEqual.*; import org.junit.Before; import org.junit.Test; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.SimpleBeanDefinitionRegistry; import org.springframework.core.env.Environment; import org.springframework.core.env.StandardEnvironment; import org.springframework.core.io.ResourceLoader; @@ -29,6 +32,7 @@ import org.springframework.core.type.StandardAnnotationMetadata; import org.springframework.data.annotation.Id; import org.springframework.data.keyvalue.repository.KeyValueRepository; import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.RedisKeyValueAdapter.EnableKeyspaceEvents; import org.springframework.data.repository.Repository; import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; import org.springframework.data.repository.config.RepositoryConfiguration; @@ -78,6 +82,25 @@ public class RedisRepositoryConfigurationExtensionUnitTests { extension.getRepositoryConfigurations(configurationSource, loader, true)); } + /** + * @see DATAREDIS-491 + */ + @Test + public void picksUpEnableKeyspaceEventsCorrectly() { + + metadata = new StandardAnnotationMetadata(ConfigWithKeyspaceEventsDisabled.class, true); + configurationSource = new AnnotationRepositoryConfigurationSource(metadata, EnableRedisRepositories.class, loader, + environment); + + RedisRepositoryConfigurationExtension extension = new RedisRepositoryConfigurationExtension(); + + BeanDefinitionRegistry beanDefintionRegistry = new SimpleBeanDefinitionRegistry(); + extension.registerBeansForRoot(beanDefintionRegistry, configurationSource); + + assertThat(beanDefintionRegistry.getBeanDefinition("redisKeyValueAdapter").getAttribute("enableKeyspaceEvents"), + equalTo((Object) EnableKeyspaceEvents.OFF)); + } + private static void assertDoesNotHaveRepo(Class repositoryInterface, Collection> configs) { @@ -108,6 +131,11 @@ public class RedisRepositoryConfigurationExtensionUnitTests { } + @EnableRedisRepositories(considerNestedRepositories = true, enableKeyspaceEvents = EnableKeyspaceEvents.OFF) + static class ConfigWithKeyspaceEventsDisabled { + + } + @RedisHash static class Sample { @Id String id;