Add Hazelcast

Fixes gh-276
This commit is contained in:
Tommy Ludwig
2015-08-23 23:57:08 +09:00
committed by Rob Winch
parent a48864bf20
commit d1c00c6080
16 changed files with 889 additions and 213 deletions

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2002-2015 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.session.data.hazelcast;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.session.ExpiringSession;
import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDeletedEvent;
import org.springframework.session.events.SessionExpiredEvent;
import org.springframework.util.Assert;
import com.hazelcast.core.EntryEvent;
import com.hazelcast.map.listener.EntryAddedListener;
import com.hazelcast.map.listener.EntryEvictedListener;
import com.hazelcast.map.listener.EntryRemovedListener;
/**
* Listen for events on the Hazelcast-backed SessionRepository and
* translate those events into the corresponding Spring Session events.
* Publish the Spring Session events with the given {@link ApplicationEventPublisher}.
* <ul>
* <li>entryAdded --> {@link SessionCreatedEvent}</li>
* <li>entryEvicted --> {@link SessionExpiredEvent}</li>
* <li>entryRemoved --> {@link SessionDeletedEvent}</li>
* </ul>
*
* @author Tommy Ludwig
* @author Mark Anderson
* @since 1.1
*/
public class SessionEntryListener implements EntryAddedListener<String, ExpiringSession>,
EntryEvictedListener<String, ExpiringSession>, EntryRemovedListener<String, ExpiringSession> {
private static final Log logger = LogFactory.getLog(SessionEntryListener.class);
private ApplicationEventPublisher eventPublisher;
public SessionEntryListener(ApplicationEventPublisher eventPublisher) {
Assert.notNull(eventPublisher, "eventPublisher cannot be null");
this.eventPublisher = eventPublisher;
}
public void entryAdded(EntryEvent<String, ExpiringSession> event) {
logger.debug("Session created with id: " + event.getValue().getId());
this.eventPublisher.publishEvent(new SessionCreatedEvent(this, event.getValue()));
}
public void entryEvicted(EntryEvent<String, ExpiringSession> event) {
logger.debug("Session expired with id: " + event.getOldValue().getId());
this.eventPublisher.publishEvent(new SessionExpiredEvent(this, event.getOldValue()));
}
public void entryRemoved(EntryEvent<String, ExpiringSession> event) {
logger.debug("Session deleted with id: " + event.getOldValue().getId());
this.eventPublisher.publishEvent(new SessionDeletedEvent(this, event.getOldValue()));
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2002-2015 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.session.data.hazelcast.config.annotation.web.http;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
/**
* Add this annotation to a {@code @Configuration} class to expose the
* SessionRepositoryFilter as a bean named "springSessionRepositoryFilter" and
* backed by Hazelcast. In order to leverage the annotation, a single {@link HazelcastInstance}
* must be provided. For example:
* <pre>
* <code>
* {@literal @Configuration}
* {@literal @EnableHazelcastHttpSession}
* public class HazelcastHttpSessionConfig {
*
* {@literal @Bean}
* public HazelcastInstance embeddedHazelcast() {
* Config hazelcastConfig = new Config();
* return Hazelcast.newHazelcastInstance(hazelcastConfig);
* }
*
* }
* </code>
* </pre>
*
* More advanced configurations can extend {@link HazelcastHttpSessionConfiguration} instead.
*
* @author Tommy Ludwig
* @since 1.1
* @see EnableSpringHttpSession
*/
@Retention(value=java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value={java.lang.annotation.ElementType.TYPE})
@Documented
@Import(HazelcastHttpSessionConfiguration.class)
@Configuration
public @interface EnableHazelcastHttpSession {
/**
* This is the session timeout in seconds. By default, it is set to 1800 seconds (30 minutes).
* This should be a non-negative integer.
* <p>If you wish to use external configuration (outside of this annotation) to set this value, you can
* set this to "" (an empty String), which will prevent this configuration from overriding
* the external configuration for this value.</p>
*
* @return the seconds a session can be inactive before expiring
*/
String maxInactiveIntervalInSeconds() default "1800";
/**
* This is the name of the Map that will be used in Hazelcast to store the session data.
* Default is "spring:session:sessions".
* @return the name of the Map to store the sessions in Hazelcast
*/
String sessionMapName() default "spring:session:sessions";
}

View File

@@ -0,0 +1,156 @@
/*
* Copyright 2002-2015 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.session.data.hazelcast.config.annotation.web.http;
import java.util.Map;
import javax.annotation.PreDestroy;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportAware;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.session.ExpiringSession;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.SessionRepository;
import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration;
import org.springframework.session.data.hazelcast.SessionEntryListener;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.util.ClassUtils;
import com.hazelcast.config.MapConfig;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.IMap;
/**
* Exposes the {@link SessionRepositoryFilter} as a bean named
* "springSessionRepositoryFilter". In order to use this a single
* {@link HazelcastInstance} must be exposed as a Bean.
*
* @author Tommy Ludwig
* @since 1.1
* @see EnableHazelcastHttpSession
*/
@Configuration
public class HazelcastHttpSessionConfiguration extends SpringHttpSessionConfiguration implements ImportAware, BeanClassLoaderAware {
/** This is the magic value to use if you do not want this configuration
* overriding the maxIdleSeconds value for the Map backing the session data. */
private static final String DO_NOT_CONFIGURE_INACTIVE_INTERVAL_STRING = "";
private ClassLoader beanClassLoader;
private Integer maxInactiveIntervalInSeconds = 1800;
private String sessionMapName = "spring:session:sessions";
private String sessionListenerUid;
private IMap<String, ExpiringSession> sessionsMap;
@Bean
public SessionRepository<ExpiringSession> sessionRepository(HazelcastInstance hazelcastInstance, SessionEntryListener sessionListener) {
configureSessionMap(hazelcastInstance);
this.sessionsMap = hazelcastInstance.getMap(sessionMapName);
this.sessionListenerUid = this.sessionsMap.addEntryListener(sessionListener, true);
MapSessionRepository sessionRepository = new MapSessionRepository(this.sessionsMap);
sessionRepository.setDefaultMaxInactiveInterval(maxInactiveIntervalInSeconds);
return sessionRepository;
}
@PreDestroy
private void removeSessionListener() {
this.sessionsMap.removeEntryListener(this.sessionListenerUid);
}
@Bean
public SessionEntryListener sessionListener(ApplicationEventPublisher eventPublisher) {
return new SessionEntryListener(eventPublisher);
}
/**
* Make a {@link MapConfig} for the given sessionMapName if one does not exist.
* Ensure that maxIdleSeconds is set to maxInactiveIntervalInSeconds for proper session expiration.
*
* @param hazelcastInstance the {@link HazelcastInstance} to configure
*/
private void configureSessionMap(HazelcastInstance hazelcastInstance) {
MapConfig sessionMapConfig = hazelcastInstance.getConfig().getMapConfig(sessionMapName);
if (this.maxInactiveIntervalInSeconds != null) {
sessionMapConfig.setMaxIdleSeconds(this.maxInactiveIntervalInSeconds);
}
}
public void setImportMetadata(AnnotationMetadata importMetadata) {
Map<String, Object> enableAttrMap = importMetadata.getAnnotationAttributes(EnableHazelcastHttpSession.class.getName());
AnnotationAttributes enableAttrs = AnnotationAttributes.fromMap(enableAttrMap);
if (enableAttrs == null) {
// search parent classes
Class<?> currentClass = ClassUtils.resolveClassName(importMetadata.getClassName(), beanClassLoader);
for (Class<?> classToInspect = currentClass; classToInspect != null; classToInspect = classToInspect.getSuperclass()) {
EnableHazelcastHttpSession enableHazelcastHttpSessionAnnotation = AnnotationUtils.findAnnotation(classToInspect, EnableHazelcastHttpSession.class);
if (enableHazelcastHttpSessionAnnotation == null) {
continue;
}
enableAttrMap = AnnotationUtils
.getAnnotationAttributes(enableHazelcastHttpSessionAnnotation);
enableAttrs = AnnotationAttributes.fromMap(enableAttrMap);
}
}
transferAnnotationAttributes(enableAttrs);
}
private void transferAnnotationAttributes(AnnotationAttributes enableAttrs) {
String maxInactiveIntervalString = enableAttrs.getString("maxInactiveIntervalInSeconds");
if (DO_NOT_CONFIGURE_INACTIVE_INTERVAL_STRING.equals(maxInactiveIntervalString)) {
this.maxInactiveIntervalInSeconds = null;
} else {
try {
this.maxInactiveIntervalInSeconds = Integer.parseInt(maxInactiveIntervalString);
} catch (NumberFormatException nfe) {
throw new IllegalArgumentException(
"@EnableHazelcastHttpSession's maxInactiveIntervalInSeconds expects an int format String but was '"
+ maxInactiveIntervalString + "' instead.", nfe);
}
}
this.sessionMapName = enableAttrs.getString("sessionMapName");
}
public void setMaxInactiveIntervalInSeconds(int maxInactiveIntervalInSeconds) {
this.maxInactiveIntervalInSeconds = maxInactiveIntervalInSeconds;
}
public String getSessionMapName() {
return this.sessionMapName;
}
public void setSessionMapName(String sessionMapName) {
this.sessionMapName = sessionMapName;
}
public void setBeanClassLoader(ClassLoader classLoader) {
this.beanClassLoader = classLoader;
}
}