From 0d899b8753bc66edb06ff9c1dfbee52c8104a8ae Mon Sep 17 00:00:00 2001 From: John Blum Date: Wed, 14 Jul 2021 15:07:42 -0700 Subject: [PATCH] Apply expiration to caching-defined Regions using @EnableExpiration annotation configuration. Resolves gh-518. --- .../annotation/ExpirationConfiguration.java | 131 +++++++++++++++--- ...pirationConfigurationIntegrationTests.java | 58 +++++++- 2 files changed, 160 insertions(+), 29 deletions(-) diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/annotation/ExpirationConfiguration.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/annotation/ExpirationConfiguration.java index ac7c7d903..07d582d97 100644 --- a/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/annotation/ExpirationConfiguration.java +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/annotation/ExpirationConfiguration.java @@ -18,7 +18,6 @@ import static org.springframework.data.gemfire.config.annotation.EnableExpiration.ExpirationPolicy; import static org.springframework.data.gemfire.config.annotation.EnableExpiration.ExpirationType; -import static org.springframework.data.gemfire.util.ArrayUtils.nullSafeArray; import static org.springframework.data.gemfire.util.CollectionUtils.nullSafeIterable; import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalStateException; @@ -29,15 +28,21 @@ import java.util.Set; import java.util.function.Supplier; +import org.apache.geode.cache.AttributesMutator; +import org.apache.geode.cache.CustomExpiry; import org.apache.geode.cache.ExpirationAction; import org.apache.geode.cache.ExpirationAttributes; import org.apache.geode.cache.Region; +import org.apache.geode.cache.RegionAttributes; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportAware; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.event.EventListener; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.type.AnnotationMetadata; import org.springframework.data.gemfire.PeerRegionFactoryBean; @@ -49,6 +54,7 @@ import org.springframework.data.gemfire.expiration.ExpiringRegionFactoryBean; import org.springframework.data.gemfire.util.ArrayUtils; import org.springframework.data.gemfire.util.CollectionUtils; +import org.springframework.data.gemfire.util.SpringUtils; import org.springframework.lang.NonNull; import org.springframework.util.Assert; @@ -102,7 +108,7 @@ public void setImportMetadata(@NonNull AnnotationMetadata importMetadata) { AnnotationAttributes[] policies = enableExpirationAttributes.getAnnotationArray("policies"); for (AnnotationAttributes expirationPolicyAttributes : - nullSafeArray(policies, AnnotationAttributes.class)) { + ArrayUtils.nullSafeArray(policies, AnnotationAttributes.class)) { this.expirationPolicyConfigurer = ComposableExpirationPolicyConfigurer.compose(this.expirationPolicyConfigurer, @@ -146,20 +152,44 @@ public Object postProcessBeforeInitialization(Object bean, String beanName) thro }; } + @SuppressWarnings("unused") + @EventListener(ContextRefreshedEvent.class) + public void expirationContextRefreshedListener(@NonNull ContextRefreshedEvent event) { + + ApplicationContext applicationContext = event.getApplicationContext(); + + for (Region region : applicationContext.getBeansOfType(Region.class).values()) { + getExpirationPolicyConfigurer().configure(region); + } + } + /** * Interface defining a contract for implementations that configure a {@link Region Region's} expiration policy. + * + * @see java.lang.FunctionalInterface */ + @FunctionalInterface protected interface ExpirationPolicyConfigurer { /** * Configures the expiration policy for the given {@link Region}. * - * @param regionFactoryBean {@link Region} object who's expiration policy will be configured. + * @param regionBean {@link Region} object who's expiration policy will be configured. * @return the given {@link Region} object. * @see org.apache.geode.cache.Region */ - Object configure(Object regionFactoryBean); + Object configure(Object regionBean); + /** + * Configures the expiration policy for the given {@link Region}. + * + * @param region {@link Region} who's expiration policy will be configured. + * @return the given {@link Region}. + * @see org.apache.geode.cache.Region + */ + default Region configure(Region region) { + return region; + } } /** @@ -242,8 +272,16 @@ private ComposableExpirationPolicyConfigurer(@NonNull ExpirationPolicyConfigurer * @inheritDoc */ @Override - public Object configure(Object regionFactoryBean) { - return this.two.configure(this.one.configure(regionFactoryBean)); + public Object configure(Object regionBean) { + return this.two.configure(this.one.configure(regionBean)); + } + + /** + * @inheritDoc + */ + @Override + public Region configure(Region region) { + return this.two.configure(this.one.configure(region)); } } @@ -261,12 +299,6 @@ protected static class ExpirationPolicyMetaData implements ExpirationPolicyConfi protected static final String[] ALL_REGIONS = new String[0]; - private final ExpirationAttributes defaultExpirationAttributes; - - private final Set regionNames = new HashSet<>(); - - private final Set types = new HashSet<>(); - /** * Factory method to construct an instance of {@link ExpirationPolicyMetaData} initialized with * the given {@link AnnotationAttributes} from the nested {@link ExpirationPolicy} annotation @@ -367,8 +399,8 @@ protected static ExpirationPolicyMetaData newExpirationPolicyMetaData(int timeou String[] regionNames, ExpirationType[] types) { return new ExpirationPolicyMetaData(newExpirationAttributes(timeout, action), - CollectionUtils.asSet(nullSafeArray(regionNames, String.class)), - CollectionUtils.asSet(nullSafeArray(types, ExpirationType.class))); + CollectionUtils.asSet(ArrayUtils.nullSafeArray(regionNames, String.class)), + CollectionUtils.asSet(ArrayUtils.nullSafeArray(types, ExpirationType.class))); } /** @@ -380,7 +412,7 @@ protected static ExpirationPolicyMetaData newExpirationPolicyMetaData(int timeou * @see ExpirationActionType */ protected static ExpirationActionType resolveAction(ExpirationActionType action) { - return Optional.ofNullable(action).orElse(DEFAULT_ACTION); + return action != null ? action : DEFAULT_ACTION; } /** @@ -394,6 +426,12 @@ protected static int resolveTimeout(int timeout) { return Math.max(timeout, DEFAULT_TIMEOUT); } + private final ExpirationAttributes defaultExpirationAttributes; + + private final Set regionNames = new HashSet<>(); + + private final Set types = new HashSet<>(); + /** * Constructs an instance of {@link ExpirationPolicyMetaData} initialized with the given expiration policy * configuraiton meta-data and {@link Region} expiration settings. @@ -442,14 +480,27 @@ protected ExpirationPolicyMetaData(ExpirationAttributes expirationAttributes, Se /** * Determines whether the given {@link Object} (e.g. Spring bean) is accepted for Eviction policy configuration. * - * @param regionFactoryBean {@link Object} being evaluated as an Eviction policy configuration candidate. + * @param regionBean {@link Object} being evaluated as an Eviction policy configuration candidate. * @return a boolean value indicating whether the {@link Object} is accepted for Eviction policy configuration. * @see #isRegionFactoryBean(Object) * @see #resolveRegionName(Object) * @see #accepts(Supplier) */ - protected boolean accepts(Object regionFactoryBean) { - return isRegionFactoryBean(regionFactoryBean) && accepts(() -> resolveRegionName(regionFactoryBean)); + protected boolean accepts(Object regionBean) { + return isRegionFactoryBean(regionBean) && accepts(() -> resolveRegionName(regionBean)); + } + + /** + * Determines whether the given {@link Region} is accepted for Eviction policy configuration. + * + * @param region {@link Region} being evaluated as a Eviction policy configuration candidate. + * @return a boolean value indicated whether the given {@link Region} is accepted as an Expiration policy + * configuration candidate. + * @see org.apache.geode.cache.Region + * @see #accepts(Supplier) + */ + protected boolean accepts(Region region) { + return region != null && accepts(() -> region.getName()); } /** @@ -530,15 +581,49 @@ protected String resolveRegionName(Object regionFactoryBean) { * @inheritDoc */ @Override - public Object configure(Object regionFactoryBean) { + public Object configure(Object regionBean) { + + return accepts(regionBean) + ? setExpirationAttributes((ExpiringRegionFactoryBean) regionBean) + : regionBean; + } + + /** + * @inheritDoc + */ + @Override + public Region configure(Region region) { + + if (accepts(region)) { + + RegionAttributes regionAttributes = region.getAttributes(); + + ExpirationAttributes expirationAttributes = defaultExpirationAttributes(); + + AttributesMutator regionAttributesMutator = region.getAttributesMutator(); + + if (SpringUtils.areNotNull(regionAttributes, regionAttributesMutator)) { + + CustomExpiry customEntryIdleTimeout = regionAttributes.getCustomEntryIdleTimeout(); + CustomExpiry customEntryTimeToLive = regionAttributes.getCustomEntryTimeToLive(); + + if (isIdleTimeout() && customEntryIdleTimeout == null) { + regionAttributesMutator.setCustomEntryIdleTimeout( + AnnotationBasedExpiration.forIdleTimeout(expirationAttributes)); + } + + if (isTimeToLive() && customEntryTimeToLive == null) { + regionAttributesMutator.setCustomEntryTimeToLive( + AnnotationBasedExpiration.forTimeToLive(expirationAttributes)); + } + } + } - return accepts(regionFactoryBean) - ? setExpirationAttributes((ExpiringRegionFactoryBean) regionFactoryBean) - : regionFactoryBean; + return region; } /** - * Returns the default, fallback {@link ExpirationAttributes}. + * Returns the default {@link ExpirationAttributes}. * * @return an {@link ExpirationAttributes} containing the defaults. * @see org.apache.geode.cache.ExpirationAttributes diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/EnableExpirationConfigurationIntegrationTests.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/EnableExpirationConfigurationIntegrationTests.java index b47abafb4..4fec5cc95 100644 --- a/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/EnableExpirationConfigurationIntegrationTests.java +++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/EnableExpirationConfigurationIntegrationTests.java @@ -13,32 +13,40 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.data.gemfire.config.annotation; import static org.assertj.core.api.Assertions.assertThat; import java.util.Optional; +import org.junit.After; +import org.junit.Test; + import org.apache.geode.cache.DataPolicy; import org.apache.geode.cache.Region; import org.apache.geode.cache.RegionShortcut; import org.apache.geode.cache.client.ClientRegionShortcut; -import org.junit.After; -import org.junit.Test; - +import org.springframework.cache.annotation.Cacheable; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; import org.springframework.data.gemfire.expiration.AnnotationBasedExpiration; +import org.springframework.data.gemfire.test.mock.annotation.EnableGemFireMockObjects; import org.springframework.data.gemfire.test.model.Person; +import org.springframework.stereotype.Service; /** - * The EnableExpirationConfigurationIntegrationTests class... + * Integration Tests for {@link EnableExpiration} and {@link ExpirationConfiguration}. * * @author John Blum - * @since 1.0.0 + * @see org.junit.Test + * @see org.apache.geode.cache.Region + * @see org.springframework.data.gemfire.config.annotation.EnableExpiration + * @see org.springframework.data.gemfire.config.annotation.ExpirationConfiguration + * @since 1.9.0 */ +@SuppressWarnings("unused") public class EnableExpirationConfigurationIntegrationTests { private static final String GEMFIRE_LOG_LEVEL = "error"; @@ -76,6 +84,16 @@ private void assertRegionExpirationConfiguration(ConfigurableApplicationContext .isInstanceOf(AnnotationBasedExpiration.class); } + @Test + public void assertApplicationCachingDefinedRegionsExpirationPoliciesAreCorrect() { + + ConfigurableApplicationContext applicationContext = newApplicationContext(ApplicationConfiguration.class); + + assertThat(applicationContext).isNotNull(); + assertRegionExpirationConfiguration(applicationContext, "CacheOne"); + assertRegionExpirationConfiguration(applicationContext, "CacheTwo"); + } + @Test public void assertClientCacheRegionExpirationPoliciesAreCorrect() { assertRegionExpirationConfiguration(newApplicationContext(ClientCacheRegionExpirationConfiguration.class), @@ -88,14 +106,42 @@ public void assertPeerCacheRegionExpirationPoliciesAreCorrect() { "People"); } + @ClientCacheApplication(name = "EnableExpirationConfigurationIntegrationTests", logLevel = GEMFIRE_LOG_LEVEL) + @EnableCachingDefinedRegions(clientRegionShortcut = ClientRegionShortcut.LOCAL) + @EnableExpiration + @EnableGemFireMockObjects + static class ApplicationConfiguration { + + @Bean + ApplicationService applicationService() { + return new ApplicationService(); + } + } + + @Service + static class ApplicationService { + + @Cacheable("CacheOne") + public Object someMethod(Object key) { + return null; + } + + @Cacheable("CacheTwo") + public Object someOtherMethod(Object key) { + return null; + } + } + @ClientCacheApplication(name = "EnableExpirationConfigurationIntegrationTests", logLevel = GEMFIRE_LOG_LEVEL) @EnableEntityDefinedRegions(basePackageClasses = Person.class, clientRegionShortcut = ClientRegionShortcut.LOCAL) @EnableExpiration + @EnableGemFireMockObjects static class ClientCacheRegionExpirationConfiguration { } @PeerCacheApplication(name = "EnableExpirationConfigurationIntegrationTests", logLevel = GEMFIRE_LOG_LEVEL) @EnableEntityDefinedRegions(basePackageClasses = Person.class, serverRegionShortcut = RegionShortcut.LOCAL) @EnableExpiration + @EnableGemFireMockObjects static class PeerCacheRegionExpirationConfiguration { } }