From 7ea48fdc6c1c1bcbb49d1c51cd3d36d8ee0cba07 Mon Sep 17 00:00:00 2001 From: John Blum Date: Mon, 22 Oct 2018 19:09:30 -0700 Subject: [PATCH] Add annotation configuration support to configure custom session expiration policies. Resolves gh-5. --- ...stractGemFireHttpSessionConfiguration.java | 21 +++ .../web/http/EnableGemFireHttpSession.java | 19 ++- .../http/GemFireHttpSessionConfiguration.java | 151 ++++++++++++++++-- 3 files changed, 176 insertions(+), 15 deletions(-) diff --git a/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/AbstractGemFireHttpSessionConfiguration.java b/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/AbstractGemFireHttpSessionConfiguration.java index 35244e6..e906bb1 100644 --- a/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/AbstractGemFireHttpSessionConfiguration.java +++ b/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/AbstractGemFireHttpSessionConfiguration.java @@ -32,6 +32,9 @@ import org.springframework.session.config.annotation.web.http.SpringHttpSessionC import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * The {@link AbstractGemFireHttpSessionConfiguration} class is an abstract base class containing configuration logic * common to Apache Geode and Pivotal GemFire in order to manage {@link javax.servlet.http.HttpSession} state. @@ -59,6 +62,8 @@ public abstract class AbstractGemFireHttpSessionConfiguration extends SpringHttp private Environment environment; + private final Logger logger = LoggerFactory.getLogger(getClass()); + /** * Sets a reference the Spring {@link ApplicationContext}. * @@ -68,7 +73,9 @@ public abstract class AbstractGemFireHttpSessionConfiguration extends SpringHttp */ @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + super.setApplicationContext(applicationContext); + this.applicationContext = applicationContext; } @@ -144,6 +151,16 @@ public abstract class AbstractGemFireHttpSessionConfiguration extends SpringHttp return this.environment; } + /** + * Return a reference to the configured SLF4J {@link Logger}. + * + * @return a reference to the configured SLF4J {@link Logger}. + * @see org.slf4j.Logger + */ + protected Logger getLogger() { + return this.logger; + } + protected String clientRegionShortcutPropertyName() { return propertyName("cache.client.region.shortcut"); } @@ -168,6 +185,10 @@ public abstract class AbstractGemFireHttpSessionConfiguration extends SpringHttp return propertyName(String.format("session.%s", propertyNameSuffix)); } + protected String sessionExpirationPolicyBeanNamePropertyName() { + return sessionPropertyName("expiration.bean-name"); + } + protected String sessionRegionNamePropertyName() { return sessionPropertyName("region.name"); } diff --git a/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/EnableGemFireHttpSession.java b/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/EnableGemFireHttpSession.java index eed4343..c5c430c 100644 --- a/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/EnableGemFireHttpSession.java +++ b/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/EnableGemFireHttpSession.java @@ -34,6 +34,7 @@ import org.apache.geode.cache.client.Pool; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.session.Session; +import org.springframework.session.data.gemfire.expiration.SessionExpirationPolicy; import org.springframework.session.data.gemfire.serialization.SessionSerializer; import org.springframework.session.web.http.SessionRepositoryFilter; @@ -178,11 +179,27 @@ public @interface EnableGemFireHttpSession { */ RegionShortcut serverRegionShortcut() default RegionShortcut.PARTITION; + /** + * Defines the name of the bean referring to the {@link SessionExpirationPolicy} used to configure + * the {@link Session} expiration logic and strategy. + * + * The {@link Object bean} referred to by its {@link String name} must be of type {@link SessionExpirationPolicy}. + * + * Defaults to unset. + + * Use the {@literal spring.session.data.gemfire.session.expiration.bean-name} in Spring Boot + * {@literal application.properties}. + * + * @return a {@link String} containing the bean name of the configured {@link SessionExpirationPolicy}. + * @see org.springframework.session.data.gemfire.expiration.SessionExpirationPolicy + */ + String sessionExpirationPolicyBeanName() default GemFireHttpSessionConfiguration.DEFAULT_SESSION_EXPIRATION_POLICY_BEAN_NAME; + /** * Defines the bean name of the {@link SessionSerializer} used to serialize {@link Session} state * between client and server or to disk when persisting or overflowing {@link Session} state. * - * The bean referred to by its name must be of type {@link SessionSerializer}. + * The {@link Object bean referred to by its {@link String name} must be of type {@link SessionSerializer}. * * Defaults to {@literal SessionPdxSerializer}. * diff --git a/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionConfiguration.java b/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionConfiguration.java index d809c64..dd6a0cc 100644 --- a/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionConfiguration.java +++ b/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionConfiguration.java @@ -16,6 +16,7 @@ package org.springframework.session.data.gemfire.config.annotation.web.http; +import java.time.Duration; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -49,6 +50,7 @@ import org.springframework.core.type.AnnotationMetadata; import org.springframework.data.gemfire.CacheFactoryBean; import org.springframework.data.gemfire.GemfireOperations; import org.springframework.data.gemfire.GemfireTemplate; +import org.springframework.data.gemfire.GemfireUtils; import org.springframework.data.gemfire.IndexFactoryBean; import org.springframework.data.gemfire.IndexType; import org.springframework.data.gemfire.RegionAttributesFactoryBean; @@ -61,6 +63,9 @@ import org.springframework.session.data.gemfire.GemFireOperationsSessionReposito import org.springframework.session.data.gemfire.config.annotation.web.http.support.GemFireCacheTypeAwareRegionFactoryBean; import org.springframework.session.data.gemfire.config.annotation.web.http.support.SessionAttributesIndexFactoryBean; import org.springframework.session.data.gemfire.config.annotation.web.http.support.SpringSessionGemFireConfigurer; +import org.springframework.session.data.gemfire.expiration.SessionExpirationPolicy; +import org.springframework.session.data.gemfire.expiration.config.SessionExpirationTimeoutAwareBeanPostProcessor; +import org.springframework.session.data.gemfire.expiration.support.SessionExpirationPolicyCustomExpiryAdapter; import org.springframework.session.data.gemfire.serialization.SessionSerializer; import org.springframework.session.data.gemfire.serialization.data.provider.DataSerializableSessionSerializer; import org.springframework.session.data.gemfire.serialization.data.support.DataSerializerSessionSerializerAdapter; @@ -87,20 +92,26 @@ import org.springframework.util.StringUtils; * @see org.springframework.context.annotation.Bean * @see org.springframework.context.annotation.Configuration * @see org.springframework.context.annotation.DependsOn + * @see org.springframework.context.annotation.Import * @see org.springframework.context.annotation.ImportAware * @see org.springframework.core.annotation.AnnotationAttributes * @see org.springframework.core.type.AnnotationMetadata + * @see org.springframework.data.gemfire.CacheFactoryBean * @see org.springframework.data.gemfire.GemfireOperations * @see org.springframework.data.gemfire.GemfireTemplate * @see org.springframework.data.gemfire.IndexFactoryBean * @see org.springframework.data.gemfire.RegionAttributesFactoryBean * @see org.springframework.session.Session + * @see org.springframework.session.SessionRepository * @see org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration * @see org.springframework.session.data.gemfire.GemFireOperationsSessionRepository * @see org.springframework.session.data.gemfire.config.annotation.web.http.AbstractGemFireHttpSessionConfiguration * @see org.springframework.session.data.gemfire.config.annotation.web.http.EnableGemFireHttpSession * @see org.springframework.session.data.gemfire.config.annotation.web.http.support.GemFireCacheTypeAwareRegionFactoryBean * @see org.springframework.session.data.gemfire.config.annotation.web.http.support.SessionAttributesIndexFactoryBean + * @see org.springframework.session.data.gemfire.expiration.SessionExpirationPolicy + * @see org.springframework.session.data.gemfire.expiration.config.SessionExpirationTimeoutAwareBeanPostProcessor + * @see org.springframework.session.data.gemfire.serialization.SessionSerializer * @since 1.1.0 */ @Configuration @@ -136,6 +147,11 @@ public class GemFireHttpSessionConfiguration extends AbstractGemFireHttpSessionC */ public static final String DEFAULT_POOL_NAME = GemfireConstants.DEFAULT_GEMFIRE_POOL_NAME; + /** + * Default name for the {@link SessionExpirationPolicy} bean. + */ + public static final String DEFAULT_SESSION_EXPIRATION_POLICY_BEAN_NAME = ""; + /** * Default name of (Client)Cache {@link Region} used to store {@link Session} state. */ @@ -165,6 +181,8 @@ public class GemFireHttpSessionConfiguration extends AbstractGemFireHttpSessionC private String sessionRegionName = DEFAULT_SESSION_REGION_NAME; + private String sessionExpirationPolicyBeanName = DEFAULT_SESSION_EXPIRATION_POLICY_BEAN_NAME; + private String sessionSerializerBeanName = DEFAULT_SESSION_SERIALIZER_BEAN_NAME; private String[] indexableSessionAttributes = DEFAULT_INDEXABLE_SESSION_ATTRIBUTES; @@ -195,7 +213,9 @@ public class GemFireHttpSessionConfiguration extends AbstractGemFireHttpSessionC * @see org.apache.geode.cache.client.ClientRegionShortcut */ protected ClientRegionShortcut getClientRegionShortcut() { - return Optional.ofNullable(this.clientRegionShortcut).orElse(DEFAULT_CLIENT_REGION_SHORTCUT); + + return Optional.ofNullable(this.clientRegionShortcut) + .orElse(DEFAULT_CLIENT_REGION_SHORTCUT); } /** @@ -216,7 +236,9 @@ public class GemFireHttpSessionConfiguration extends AbstractGemFireHttpSessionC * for which an Index will be created. Defaults to an empty array if unspecified. */ protected String[] getIndexableSessionAttributes() { - return Optional.ofNullable(this.indexableSessionAttributes).orElse(DEFAULT_INDEXABLE_SESSION_ATTRIBUTES); + + return Optional.ofNullable(this.indexableSessionAttributes) + .orElse(DEFAULT_INDEXABLE_SESSION_ATTRIBUTES); } /** @@ -259,7 +281,10 @@ public class GemFireHttpSessionConfiguration extends AbstractGemFireHttpSessionC * @see org.apache.geode.cache.client.Pool#getName() */ protected String getPoolName() { - return Optional.ofNullable(this.poolName).filter(StringUtils::hasText).orElse(DEFAULT_POOL_NAME); + + return Optional.ofNullable(this.poolName) + .filter(StringUtils::hasText) + .orElse(DEFAULT_POOL_NAME); } /** @@ -288,7 +313,19 @@ public class GemFireHttpSessionConfiguration extends AbstractGemFireHttpSessionC * @see org.apache.geode.cache.RegionShortcut */ protected RegionShortcut getServerRegionShortcut() { - return Optional.ofNullable(this.serverRegionShortcut).orElse(DEFAULT_SERVER_REGION_SHORTCUT); + + return Optional.ofNullable(this.serverRegionShortcut) + .orElse(DEFAULT_SERVER_REGION_SHORTCUT); + } + + public void setSessionExpirationPolicyBeanName(String sessionExpirationPolicyBeanName) { + this.sessionExpirationPolicyBeanName = sessionExpirationPolicyBeanName; + } + + protected Optional getSessionExpirationPolicyBeanName() { + + return Optional.ofNullable(this.sessionExpirationPolicyBeanName) + .filter(StringUtils::hasText); } /** @@ -310,7 +347,9 @@ public class GemFireHttpSessionConfiguration extends AbstractGemFireHttpSessionC * @see org.apache.geode.cache.Region#getName() */ protected String getSessionRegionName() { - return Optional.ofNullable(this.sessionRegionName).filter(StringUtils::hasText) + + return Optional.ofNullable(this.sessionRegionName) + .filter(StringUtils::hasText) .orElse(DEFAULT_SESSION_REGION_NAME); } @@ -346,7 +385,9 @@ public class GemFireHttpSessionConfiguration extends AbstractGemFireHttpSessionC * @see org.springframework.session.data.gemfire.serialization.SessionSerializer */ protected String getSessionSerializerBeanName() { - return Optional.ofNullable(this.sessionSerializerBeanName).filter(StringUtils::hasText) + + return Optional.ofNullable(this.sessionSerializerBeanName) + .filter(StringUtils::hasText) .orElse(DEFAULT_SESSION_SERIALIZER_BEAN_NAME); } @@ -376,45 +417,83 @@ public class GemFireHttpSessionConfiguration extends AbstractGemFireHttpSessionC AnnotationAttributes.fromMap(importMetadata.getAnnotationAttributes( EnableGemFireHttpSession.class.getName())); + configureClientRegionShortcut(enableGemFireHttpSessionAttributes); + configureIndexableSessionAttributes(enableGemFireHttpSessionAttributes); + configureMaxInactiveIntervalInSeconds(enableGemFireHttpSessionAttributes); + configurePoolName(enableGemFireHttpSessionAttributes); + configureServerRegionShortcut(enableGemFireHttpSessionAttributes); + configureSessionExpirationPolicyBeanName(enableGemFireHttpSessionAttributes); + configureSessionRegionName(enableGemFireHttpSessionAttributes); + configureSessionSerializerBeanName(enableGemFireHttpSessionAttributes); + + applySpringSessionGemFireConfigurer(); + } + + private void configureClientRegionShortcut(AnnotationAttributes enableGemFireHttpSessionAttributes) { + ClientRegionShortcut defaultClientRegionShortcut = enableGemFireHttpSessionAttributes.getEnum("clientRegionShortcut"); setClientRegionShortcut(resolveProperty(clientRegionShortcutPropertyName(), ClientRegionShortcut.class, defaultClientRegionShortcut)); + } + + private void configureIndexableSessionAttributes(AnnotationAttributes enableGemFireHttpSessionAttributes) { String[] defaultIndexableSessionAttributes = enableGemFireHttpSessionAttributes.getStringArray("indexableSessionAttributes"); setIndexableSessionAttributes(resolveProperty(indexableSessionAttributesPropertyName(), defaultIndexableSessionAttributes)); + } + + private void configureMaxInactiveIntervalInSeconds(AnnotationAttributes enableGemFireHttpSessionAttributes) { Integer defaultMaxInactiveIntervalInSeconds = enableGemFireHttpSessionAttributes.getNumber("maxInactiveIntervalInSeconds").intValue(); setMaxInactiveIntervalInSeconds(resolveProperty(maxInactiveIntervalInSecondsPropertyName(), defaultMaxInactiveIntervalInSeconds)); + } + private void configurePoolName(AnnotationAttributes enableGemFireHttpSessionAttributes) { String defaultPoolName = enableGemFireHttpSessionAttributes.getString("poolName"); setPoolName(resolveProperty(poolNamePropertyName(), defaultPoolName)); + } - String defaultSessionRegionName = enableGemFireHttpSessionAttributes.getString("regionName"); - - setSessionRegionName(resolveProperty(sessionRegionNamePropertyName(), defaultSessionRegionName)); + private void configureServerRegionShortcut(AnnotationAttributes enableGemFireHttpSessionAttributes) { RegionShortcut defaultServerRegionShortcut = enableGemFireHttpSessionAttributes.getEnum("serverRegionShortcut"); setServerRegionShortcut(resolveProperty(serverRegionShortcutPropertyName(), RegionShortcut.class, defaultServerRegionShortcut)); + } + + private void configureSessionExpirationPolicyBeanName(AnnotationAttributes enableGemFireHttpSessionAttributes) { + + String defaultSessionExpirationPolicyBeanName = + enableGemFireHttpSessionAttributes.getString("sessionExpirationPolicyBeanName"); + + setSessionExpirationPolicyBeanName(resolveProperty(sessionExpirationPolicyBeanNamePropertyName(), + defaultSessionExpirationPolicyBeanName)); + } + + private void configureSessionRegionName(AnnotationAttributes enableGemFireHttpSessionAttributes) { + + String defaultSessionRegionName = enableGemFireHttpSessionAttributes.getString("regionName"); + + setSessionRegionName(resolveProperty(sessionRegionNamePropertyName(), defaultSessionRegionName)); + } + + private void configureSessionSerializerBeanName(AnnotationAttributes enableGemFireHttpSessionAttributes) { String defaultSessionSerializerBeanName = enableGemFireHttpSessionAttributes.getString("sessionSerializerBeanName"); setSessionSerializerBeanName(resolveProperty(sessionSerializerBeanNamePropertyName(), defaultSessionSerializerBeanName)); - - applySpringSessionGemFireConfigurer(); } private Optional resolveSpringSessionGemFireConfigurer() { @@ -454,6 +533,36 @@ public class GemFireHttpSessionConfiguration extends AbstractGemFireHttpSessionC getBeanFactory().registerAlias(getSessionSerializerBeanName(), SESSION_SERIALIZER_BEAN_ALIAS); } + @Bean + BeanPostProcessor sessionExpirationTimeoutAwareBeanPostProcessor() { + + Duration expirationTimeout = Duration.ofSeconds(getMaxInactiveIntervalInSeconds()); + + return new SessionExpirationTimeoutAwareBeanPostProcessor(expirationTimeout); + } + + private Optional resolveSessionExpirationPolicy() { + + Optional sessionExpirationPolicyBeanName = getSessionExpirationPolicyBeanName(); + + if (sessionExpirationPolicyBeanName.isPresent()) { + if (getApplicationContext().containsBean(sessionExpirationPolicyBeanName.get())) { + return Optional.of(getApplicationContext() + .getBean(sessionExpirationPolicyBeanName.get(), SessionExpirationPolicy.class)); + } + else { + + String logMessage = "No Bean with name [{}] and type [{}] was configured;" + + " Defaulting to Expiration policy configured for Region [{}]"; + + getLogger().warn(logMessage, sessionExpirationPolicyBeanName.get(), + SessionExpirationPolicy.class.getName(), getSessionRegionName()); + } + } + + return Optional.empty(); + } + @Bean BeanPostProcessor sessionSerializerConfigurationBeanPostProcessor() { @@ -576,9 +685,23 @@ public class GemFireHttpSessionConfiguration extends AbstractGemFireHttpSessionC regionAttributes.setValueConstraint(SESSION_REGION_VALUE_CONSTRAINT); if (isExpirationAllowed(gemfireCache)) { + regionAttributes.setStatisticsEnabled(true); - regionAttributes.setEntryIdleTimeout( - new ExpirationAttributes(Math.max(getMaxInactiveIntervalInSeconds(), 0), ExpirationAction.INVALIDATE)); + + Optional sessionExpirationPolicy = resolveSessionExpirationPolicy(); + + if (sessionExpirationPolicy.isPresent()) { + regionAttributes.setCustomEntryIdleTimeout(new SessionExpirationPolicyCustomExpiryAdapter( + sessionExpirationPolicy.get())); + } + else { + regionAttributes.setEntryIdleTimeout(new ExpirationAttributes( + Math.max(getMaxInactiveIntervalInSeconds(), 0), ExpirationAction.INVALIDATE)); + } + } + else { + getLogger().info("Expiration is not allowed on Regions with a data management policy of {}", + GemfireUtils.isClient(gemfireCache) ? getClientRegionShortcut() : getServerRegionShortcut()); } return regionAttributes; @@ -634,7 +757,7 @@ public class GemFireHttpSessionConfiguration extends AbstractGemFireHttpSessionC */ @Bean public GemFireOperationsSessionRepository sessionRepository( - @Qualifier("sessionRegionTemplate") GemfireOperations gemfireOperations) { + @Qualifier("sessionRegionTemplate") GemfireOperations gemfireOperations) { GemFireOperationsSessionRepository sessionRepository = new GemFireOperationsSessionRepository(gemfireOperations);