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.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.Optional;
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.ExpirationActionType;
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 class ExpirationConfiguration extends AbstractAnnotationConfigSupport imp
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 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.
*
* @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 @@ public class ExpirationConfiguration extends AbstractAnnotationConfigSupport imp
* @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 @@ public class ExpirationConfiguration extends AbstractAnnotationConfigSupport imp
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
* the given {@link AnnotationAttributes} from the nested {@link ExpirationPolicy} annotation
@@ -367,8 +399,8 @@ public class ExpirationConfiguration extends AbstractAnnotationConfigSupport imp
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 @@ public class ExpirationConfiguration extends AbstractAnnotationConfigSupport imp
* @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 @@ public class ExpirationConfiguration extends AbstractAnnotationConfigSupport imp
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
* 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.
*
* @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 @@ public class ExpirationConfiguration extends AbstractAnnotationConfigSupport imp
* @inheritDoc
*/
@Override
public Object configure(Object regionFactoryBean) {
public Object configure(Object regionBean) {
return accepts(regionFactoryBean)
? setExpirationAttributes((ExpiringRegionFactoryBean<?, ?>) regionFactoryBean)
: regionFactoryBean;
return accepts(regionBean)
? setExpirationAttributes((ExpiringRegionFactoryBean<?, ?>) regionBean)
: 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.
* @see org.apache.geode.cache.ExpirationAttributes

View File

@@ -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 @@ public class EnableExpirationConfigurationIntegrationTests {
.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 class EnableExpirationConfigurationIntegrationTests {
"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 { }
}