Apply expiration to caching-defined Regions using @EnableExpiration annotation configuration.

Resolves gh-518.
This commit is contained in:
John Blum
2021-07-14 15:07:42 -07:00
parent df41ee32ef
commit 0d899b8753
2 changed files with 160 additions and 29 deletions

View File

@@ -18,7 +18,6 @@ package org.springframework.data.gemfire.config.annotation;
import static org.springframework.data.gemfire.config.annotation.EnableExpiration.ExpirationPolicy; 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.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.CollectionUtils.nullSafeIterable;
import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalStateException; import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalStateException;
@@ -29,15 +28,21 @@ import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.function.Supplier; 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.ExpirationAction;
import org.apache.geode.cache.ExpirationAttributes; import org.apache.geode.cache.ExpirationAttributes;
import org.apache.geode.cache.Region; import org.apache.geode.cache.Region;
import org.apache.geode.cache.RegionAttributes;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportAware; 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.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.gemfire.PeerRegionFactoryBean; import org.springframework.data.gemfire.PeerRegionFactoryBean;
@@ -49,6 +54,7 @@ import org.springframework.data.gemfire.expiration.ExpirationActionType;
import org.springframework.data.gemfire.expiration.ExpiringRegionFactoryBean; import org.springframework.data.gemfire.expiration.ExpiringRegionFactoryBean;
import org.springframework.data.gemfire.util.ArrayUtils; import org.springframework.data.gemfire.util.ArrayUtils;
import org.springframework.data.gemfire.util.CollectionUtils; import org.springframework.data.gemfire.util.CollectionUtils;
import org.springframework.data.gemfire.util.SpringUtils;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@@ -102,7 +108,7 @@ public class ExpirationConfiguration extends AbstractAnnotationConfigSupport imp
AnnotationAttributes[] policies = enableExpirationAttributes.getAnnotationArray("policies"); AnnotationAttributes[] policies = enableExpirationAttributes.getAnnotationArray("policies");
for (AnnotationAttributes expirationPolicyAttributes : for (AnnotationAttributes expirationPolicyAttributes :
nullSafeArray(policies, AnnotationAttributes.class)) { ArrayUtils.nullSafeArray(policies, AnnotationAttributes.class)) {
this.expirationPolicyConfigurer = this.expirationPolicyConfigurer =
ComposableExpirationPolicyConfigurer.compose(this.expirationPolicyConfigurer, ComposableExpirationPolicyConfigurer.compose(this.expirationPolicyConfigurer,
@@ -146,20 +152,44 @@ public class ExpirationConfiguration extends AbstractAnnotationConfigSupport imp
}; };
} }
@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. * Interface defining a contract for implementations that configure a {@link Region Region's} expiration policy.
*
* @see java.lang.FunctionalInterface
*/ */
@FunctionalInterface
protected interface ExpirationPolicyConfigurer { protected interface ExpirationPolicyConfigurer {
/** /**
* Configures the expiration policy for the given {@link Region}. * 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. * @return the given {@link Region} object.
* @see org.apache.geode.cache.Region * @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 @@ public class ExpirationConfiguration extends AbstractAnnotationConfigSupport imp
* @inheritDoc * @inheritDoc
*/ */
@Override @Override
public Object configure(Object regionFactoryBean) { public Object configure(Object regionBean) {
return this.two.configure(this.one.configure(regionFactoryBean)); 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 @@ public class ExpirationConfiguration extends AbstractAnnotationConfigSupport imp
protected static final String[] ALL_REGIONS = new String[0]; protected static final String[] ALL_REGIONS = new String[0];
private final ExpirationAttributes defaultExpirationAttributes;
private final Set<String> regionNames = new HashSet<>();
private final Set<ExpirationType> types = new HashSet<>();
/** /**
* Factory method to construct an instance of {@link ExpirationPolicyMetaData} initialized with * Factory method to construct an instance of {@link ExpirationPolicyMetaData} initialized with
* the given {@link AnnotationAttributes} from the nested {@link ExpirationPolicy} annotation * the given {@link AnnotationAttributes} from the nested {@link ExpirationPolicy} annotation
@@ -367,8 +399,8 @@ public class ExpirationConfiguration extends AbstractAnnotationConfigSupport imp
String[] regionNames, ExpirationType[] types) { String[] regionNames, ExpirationType[] types) {
return new ExpirationPolicyMetaData(newExpirationAttributes(timeout, action), return new ExpirationPolicyMetaData(newExpirationAttributes(timeout, action),
CollectionUtils.asSet(nullSafeArray(regionNames, String.class)), CollectionUtils.asSet(ArrayUtils.nullSafeArray(regionNames, String.class)),
CollectionUtils.asSet(nullSafeArray(types, ExpirationType.class))); CollectionUtils.asSet(ArrayUtils.nullSafeArray(types, ExpirationType.class)));
} }
/** /**
@@ -380,7 +412,7 @@ public class ExpirationConfiguration extends AbstractAnnotationConfigSupport imp
* @see ExpirationActionType * @see ExpirationActionType
*/ */
protected static ExpirationActionType resolveAction(ExpirationActionType action) { protected static ExpirationActionType resolveAction(ExpirationActionType action) {
return Optional.ofNullable(action).orElse(DEFAULT_ACTION); return action != null ? action : DEFAULT_ACTION;
} }
/** /**
@@ -394,6 +426,12 @@ public class ExpirationConfiguration extends AbstractAnnotationConfigSupport imp
return Math.max(timeout, DEFAULT_TIMEOUT); return Math.max(timeout, DEFAULT_TIMEOUT);
} }
private final ExpirationAttributes defaultExpirationAttributes;
private final Set<String> regionNames = new HashSet<>();
private final Set<ExpirationType> types = new HashSet<>();
/** /**
* Constructs an instance of {@link ExpirationPolicyMetaData} initialized with the given expiration policy * Constructs an instance of {@link ExpirationPolicyMetaData} initialized with the given expiration policy
* configuraiton meta-data and {@link Region} expiration settings. * configuraiton meta-data and {@link Region} expiration settings.
@@ -442,14 +480,27 @@ public class ExpirationConfiguration extends AbstractAnnotationConfigSupport imp
/** /**
* Determines whether the given {@link Object} (e.g. Spring bean) is accepted for Eviction policy configuration. * 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. * @return a boolean value indicating whether the {@link Object} is accepted for Eviction policy configuration.
* @see #isRegionFactoryBean(Object) * @see #isRegionFactoryBean(Object)
* @see #resolveRegionName(Object) * @see #resolveRegionName(Object)
* @see #accepts(Supplier) * @see #accepts(Supplier)
*/ */
protected boolean accepts(Object regionFactoryBean) { protected boolean accepts(Object regionBean) {
return isRegionFactoryBean(regionFactoryBean) && accepts(() -> resolveRegionName(regionFactoryBean)); 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 @@ public class ExpirationConfiguration extends AbstractAnnotationConfigSupport imp
* @inheritDoc * @inheritDoc
*/ */
@Override @Override
public Object configure(Object regionFactoryBean) { public Object configure(Object regionBean) {
return accepts(regionFactoryBean) return accepts(regionBean)
? setExpirationAttributes((ExpiringRegionFactoryBean<?, ?>) regionFactoryBean) ? setExpirationAttributes((ExpiringRegionFactoryBean<?, ?>) regionBean)
: regionFactoryBean; : regionBean;
} }
/** /**
* Returns the default, fallback {@link ExpirationAttributes}. * @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 region;
}
/**
* Returns the default {@link ExpirationAttributes}.
* *
* @return an {@link ExpirationAttributes} containing the defaults. * @return an {@link ExpirationAttributes} containing the defaults.
* @see org.apache.geode.cache.ExpirationAttributes * @see org.apache.geode.cache.ExpirationAttributes

View File

@@ -13,32 +13,40 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.data.gemfire.config.annotation; package org.springframework.data.gemfire.config.annotation;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import java.util.Optional; import java.util.Optional;
import org.junit.After;
import org.junit.Test;
import org.apache.geode.cache.DataPolicy; import org.apache.geode.cache.DataPolicy;
import org.apache.geode.cache.Region; import org.apache.geode.cache.Region;
import org.apache.geode.cache.RegionShortcut; import org.apache.geode.cache.RegionShortcut;
import org.apache.geode.cache.client.ClientRegionShortcut; import org.apache.geode.cache.client.ClientRegionShortcut;
import org.junit.After; import org.springframework.cache.annotation.Cacheable;
import org.junit.Test;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.data.gemfire.expiration.AnnotationBasedExpiration; 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.data.gemfire.test.model.Person;
import org.springframework.stereotype.Service;
/** /**
* The EnableExpirationConfigurationIntegrationTests class... * Integration Tests for {@link EnableExpiration} and {@link ExpirationConfiguration}.
* *
* @author John Blum * @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 { public class EnableExpirationConfigurationIntegrationTests {
private static final String GEMFIRE_LOG_LEVEL = "error"; private static final String GEMFIRE_LOG_LEVEL = "error";
@@ -76,6 +84,16 @@ public class EnableExpirationConfigurationIntegrationTests {
.isInstanceOf(AnnotationBasedExpiration.class); .isInstanceOf(AnnotationBasedExpiration.class);
} }
@Test
public void assertApplicationCachingDefinedRegionsExpirationPoliciesAreCorrect() {
ConfigurableApplicationContext applicationContext = newApplicationContext(ApplicationConfiguration.class);
assertThat(applicationContext).isNotNull();
assertRegionExpirationConfiguration(applicationContext, "CacheOne");
assertRegionExpirationConfiguration(applicationContext, "CacheTwo");
}
@Test @Test
public void assertClientCacheRegionExpirationPoliciesAreCorrect() { public void assertClientCacheRegionExpirationPoliciesAreCorrect() {
assertRegionExpirationConfiguration(newApplicationContext(ClientCacheRegionExpirationConfiguration.class), assertRegionExpirationConfiguration(newApplicationContext(ClientCacheRegionExpirationConfiguration.class),
@@ -88,14 +106,42 @@ public class EnableExpirationConfigurationIntegrationTests {
"People"); "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) @ClientCacheApplication(name = "EnableExpirationConfigurationIntegrationTests", logLevel = GEMFIRE_LOG_LEVEL)
@EnableEntityDefinedRegions(basePackageClasses = Person.class, clientRegionShortcut = ClientRegionShortcut.LOCAL) @EnableEntityDefinedRegions(basePackageClasses = Person.class, clientRegionShortcut = ClientRegionShortcut.LOCAL)
@EnableExpiration @EnableExpiration
@EnableGemFireMockObjects
static class ClientCacheRegionExpirationConfiguration { } static class ClientCacheRegionExpirationConfiguration { }
@PeerCacheApplication(name = "EnableExpirationConfigurationIntegrationTests", logLevel = GEMFIRE_LOG_LEVEL) @PeerCacheApplication(name = "EnableExpirationConfigurationIntegrationTests", logLevel = GEMFIRE_LOG_LEVEL)
@EnableEntityDefinedRegions(basePackageClasses = Person.class, serverRegionShortcut = RegionShortcut.LOCAL) @EnableEntityDefinedRegions(basePackageClasses = Person.class, serverRegionShortcut = RegionShortcut.LOCAL)
@EnableExpiration @EnableExpiration
@EnableGemFireMockObjects
static class PeerCacheRegionExpirationConfiguration { } static class PeerCacheRegionExpirationConfiguration { }
} }