Add HazelcastSessionRepository

This commit improves existing Hazelcast support, which is based on
MapSessionRepository, with dedicated HazelcastSessionRepository
that implements the FindByIndexNameSessionRepository contract.

Also a new hazelcast-spring-session module was added to provide
dependency management for Hazelcast support.

Fixes gh-544
This commit is contained in:
Vedran Pavic
2016-06-09 19:13:28 +02:00
committed by Rob Winch
parent d8c3a4dd61
commit 6e04d903ae
22 changed files with 988 additions and 234 deletions

View File

@@ -21,8 +21,7 @@ import com.hazelcast.core.IMap;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.session.ExpiringSession;
import org.springframework.session.SessionRepository;
import org.springframework.session.MapSession;
import static org.assertj.core.api.Assertions.assertThat;
@@ -32,20 +31,21 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Tommy Ludwig
* @author Vedran Pavic
*/
public abstract class AbstractHazelcastRepositoryITests<S extends ExpiringSession> {
public abstract class AbstractHazelcastRepositoryITests {
@Autowired
private HazelcastInstance hazelcast;
@Autowired
private SessionRepository<S> repository;
private HazelcastSessionRepository repository;
@Test
public void createAndDestroySession() {
S sessionToSave = this.repository.createSession();
MapSession sessionToSave = this.repository.createSession();
String sessionId = sessionToSave.getId();
IMap<String, S> hazelcastMap = this.hazelcast.getMap("spring:session:sessions");
IMap<String, MapSession> hazelcastMap = this.hazelcast.getMap(
"spring:session:sessions");
assertThat(hazelcastMap.size()).isEqualTo(0);

View File

@@ -25,7 +25,6 @@ import org.junit.runner.RunWith;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.ExpiringSession;
import org.springframework.session.hazelcast.config.annotation.web.http.EnableHazelcastHttpSession;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@@ -43,8 +42,7 @@ import org.springframework.util.SocketUtils;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WebAppConfiguration
public class HazelcastClientRepositoryITests<S extends ExpiringSession>
extends AbstractHazelcastRepositoryITests<S> {
public class HazelcastClientRepositoryITests extends AbstractHazelcastRepositoryITests {
private static final int PORT = SocketUtils.findAvailableTcpPort();

View File

@@ -17,6 +17,8 @@
package org.springframework.session.hazelcast;
import com.hazelcast.config.Config;
import com.hazelcast.config.MapAttributeConfig;
import com.hazelcast.config.MapIndexConfig;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
@@ -38,8 +40,20 @@ public final class HazelcastITestUtils {
* @return the Hazelcast instance
*/
public static HazelcastInstance embeddedHazelcastServer(int port) {
MapAttributeConfig attributeConfig = new MapAttributeConfig()
.setName(HazelcastSessionRepository.PRINCIPAL_NAME_ATTRIBUTE)
.setExtractor(PrincipalNameExtractor.class.getName());
Config config = new Config();
config.getNetworkConfig().setPort(port);
config.getNetworkConfig()
.setPort(port);
config.getMapConfig("spring:session:sessions")
.addMapAttributeConfig(attributeConfig)
.addMapIndexConfig(new MapIndexConfig(
HazelcastSessionRepository.PRINCIPAL_NAME_ATTRIBUTE, false));
return Hazelcast.newHazelcastInstance(config);
}

View File

@@ -21,7 +21,6 @@ import org.junit.runner.RunWith;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.ExpiringSession;
import org.springframework.session.hazelcast.config.annotation.web.http.EnableHazelcastHttpSession;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@@ -37,8 +36,7 @@ import org.springframework.test.context.web.WebAppConfiguration;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WebAppConfiguration
public class HazelcastServerRepositoryITests<S extends ExpiringSession>
extends AbstractHazelcastRepositoryITests<S> {
public class HazelcastServerRepositoryITests extends AbstractHazelcastRepositoryITests {
@EnableHazelcastHttpSession
@Configuration

View File

@@ -0,0 +1,245 @@
/*
* Copyright 2014-2016 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.hazelcast;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import com.hazelcast.core.EntryEvent;
import com.hazelcast.core.IMap;
import com.hazelcast.map.listener.EntryAddedListener;
import com.hazelcast.map.listener.EntryEvictedListener;
import com.hazelcast.map.listener.EntryRemovedListener;
import com.hazelcast.query.Predicates;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.MapSession;
import org.springframework.session.events.AbstractSessionEvent;
import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDeletedEvent;
import org.springframework.session.events.SessionExpiredEvent;
import org.springframework.util.Assert;
/**
* A {@link org.springframework.session.SessionRepository} implementation that stores
* sessions in Hazelcast's distributed {@link IMap}.
*
* <p>
* An example of how to create a new instance can be seen below:
*
* <pre class="code">
* Config config = new Config();
*
* // ... configure Hazelcast ...
*
* HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(config);
*
* IMap{@code <String, MapSession>} sessions = hazelcastInstance
* .getMap("spring:session:sessions");
*
* HazelcastSessionRepository sessionRepository =
* new HazelcastSessionRepository(sessions);
* </pre>
*
* In order to support finding sessions by principal name using
* {@link #findByIndexNameAndIndexValue(String, String)} method, custom configuration of
* {@code IMap} supplied to this implementation is required.
*
* The following snippet demonstrates how to define required configuration using
* programmatic Hazelcast Configuration:
*
* <pre class="code">
* MapAttributeConfig attributeConfig = new MapAttributeConfig()
* .setName(HazelcastSessionRepository.PRINCIPAL_NAME_ATTRIBUTE)
* .setExtractor(PrincipalNameExtractor.class.getName());
*
* Config config = new Config();
*
* config.getMapConfig("spring:session:sessions")
* .addMapAttributeConfig(attributeConfig)
* .addMapIndexConfig(new MapIndexConfig(
* HazelcastSessionRepository.PRINCIPAL_NAME_ATTRIBUTE, false));
*
* Hazelcast.newHazelcastInstance(config);
* </pre>
*
* This implementation listens for events on the Hazelcast-backed SessionRepository and
* translates 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 Vedran Pavic
* @author Tommy Ludwig
* @author Mark Anderson
* @since 1.3.0
*/
public class HazelcastSessionRepository implements
FindByIndexNameSessionRepository<MapSession>,
EntryAddedListener<String, MapSession>,
EntryEvictedListener<String, MapSession>,
EntryRemovedListener<String, MapSession> {
/**
* The principal name custom attribute name.
*/
public static final String PRINCIPAL_NAME_ATTRIBUTE = "principalName";
private static final Log logger = LogFactory.getLog(HazelcastSessionRepository.class);
private final IMap<String, MapSession> sessions;
private ApplicationEventPublisher eventPublisher = new ApplicationEventPublisher() {
public void publishEvent(ApplicationEvent event) {
}
public void publishEvent(Object event) {
}
};
/**
* If non-null, this value is used to override
* {@link MapSession#setMaxInactiveIntervalInSeconds(int)}.
*/
private Integer defaultMaxInactiveInterval;
private String sessionListenerId;
public HazelcastSessionRepository(IMap<String, MapSession> sessions) {
Assert.notNull(sessions, "Sessions IMap must not be null");
this.sessions = sessions;
}
@PostConstruct
private void init() {
this.sessionListenerId = this.sessions.addEntryListener(this, true);
}
@PreDestroy
private void close() {
this.sessions.removeEntryListener(this.sessionListenerId);
}
/**
* Sets the {@link ApplicationEventPublisher} that is used to publish
* {@link AbstractSessionEvent session events}. The default is to not publish session
* events.
*
* @param applicationEventPublisher the {@link ApplicationEventPublisher} that is used
* to publish session events. Cannot be null.
*/
public void setApplicationEventPublisher(
ApplicationEventPublisher applicationEventPublisher) {
Assert.notNull(applicationEventPublisher,
"ApplicationEventPublisher cannot be null");
this.eventPublisher = applicationEventPublisher;
}
/**
* Set the maximum inactive interval in seconds between requests before newly created
* sessions will be invalidated. A negative time indicates that the session will never
* timeout. The default is 1800 (30 minutes).
* @param defaultMaxInactiveInterval the maximum inactive interval in seconds
*/
public void setDefaultMaxInactiveInterval(Integer defaultMaxInactiveInterval) {
this.defaultMaxInactiveInterval = defaultMaxInactiveInterval;
}
public MapSession createSession() {
MapSession result = new MapSession();
if (this.defaultMaxInactiveInterval != null) {
result.setMaxInactiveIntervalInSeconds(this.defaultMaxInactiveInterval);
}
return result;
}
public void save(MapSession session) {
this.sessions.put(session.getId(), session,
session.getMaxInactiveIntervalInSeconds(), TimeUnit.SECONDS);
}
public MapSession getSession(String id) {
MapSession saved = this.sessions.get(id);
if (saved == null) {
return null;
}
if (saved.isExpired()) {
delete(saved.getId());
return null;
}
return saved;
}
public void delete(String id) {
this.sessions.remove(id);
}
public Map<String, MapSession> findByIndexNameAndIndexValue(
String indexName, String indexValue) {
if (!PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) {
return Collections.emptyMap();
}
Collection<MapSession> sessions = this.sessions.values(
Predicates.equal(PRINCIPAL_NAME_ATTRIBUTE, indexValue));
Map<String, MapSession> sessionMap = new HashMap<String, MapSession>(
sessions.size());
for (MapSession session : sessions) {
sessionMap.put(session.getId(), session);
}
return sessionMap;
}
public void entryAdded(EntryEvent<String, MapSession> event) {
if (logger.isDebugEnabled()) {
logger.debug("Session created with id: " + event.getValue().getId());
}
this.eventPublisher.publishEvent(new SessionCreatedEvent(this, event.getValue()));
}
public void entryEvicted(EntryEvent<String, MapSession> event) {
if (logger.isDebugEnabled()) {
logger.debug("Session expired with id: " + event.getOldValue().getId());
}
this.eventPublisher
.publishEvent(new SessionExpiredEvent(this, event.getOldValue()));
}
public void entryRemoved(EntryEvent<String, MapSession> event) {
if (logger.isDebugEnabled()) {
logger.debug("Session deleted with id: " + event.getOldValue().getId());
}
this.eventPublisher
.publishEvent(new SessionDeletedEvent(this, event.getOldValue()));
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2014-2016 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.hazelcast;
import com.hazelcast.query.extractor.ValueCollector;
import com.hazelcast.query.extractor.ValueExtractor;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.MapSession;
import org.springframework.session.Session;
/**
* Hazelcast {@link ValueExtractor} responsible for extracting principal name from the
* {@link MapSession}.
*
* @author Vedran Pavic
* @since 1.3.0
*/
public class PrincipalNameExtractor extends ValueExtractor<MapSession, String> {
private static final PrincipalNameResolver PRINCIPAL_NAME_RESOLVER =
new PrincipalNameResolver();
@SuppressWarnings("unchecked")
public void extract(MapSession target, String argument,
ValueCollector collector) {
String principalName = PRINCIPAL_NAME_RESOLVER.resolvePrincipal(target);
if (principalName != null) {
collector.addObject(principalName);
}
}
/**
* Resolves the Spring Security principal name.
*/
static class PrincipalNameResolver {
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
private SpelExpressionParser parser = new SpelExpressionParser();
public String resolvePrincipal(Session session) {
String principalName = session.getAttribute(
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
if (principalName != null) {
return principalName;
}
Object authentication = session.getAttribute(SPRING_SECURITY_CONTEXT);
if (authentication != null) {
Expression expression = this.parser
.parseExpression("authentication?.name");
return expression.getValue(authentication, String.class);
}
return null;
}
}
}

View File

@@ -43,7 +43,9 @@ import org.springframework.util.Assert;
* @author Tommy Ludwig
* @author Mark Anderson
* @since 1.1
* @deprecated Use {@link HazelcastSessionRepository} instead.
*/
@Deprecated
public class SessionEntryListener implements EntryAddedListener<String, ExpiringSession>,
EntryEvictedListener<String, ExpiringSession>,
EntryRemovedListener<String, ExpiringSession> {

View File

@@ -22,6 +22,7 @@ import java.lang.annotation.Target;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.session.MapSession;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
/**
@@ -56,19 +57,20 @@ import org.springframework.session.config.annotation.web.http.EnableSpringHttpSe
@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.
*
* @return the seconds a session can be inactive before expiring
*/
int maxInactiveIntervalInSeconds() default 1800;
int maxInactiveIntervalInSeconds() default MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
/**
* This is the name of the Map that will be used in Hazelcast to store the session
* data. Default is "spring:session:sessions".
* data. Default is {@link HazelcastHttpSessionConfiguration#DEFAULT_SESSION_MAP_NAME}.
* @return the name of the Map to store the sessions in Hazelcast
*/
String sessionMapName() default "spring:session:sessions";
String sessionMapName() default HazelcastHttpSessionConfiguration.DEFAULT_SESSION_MAP_NAME;
}

View File

@@ -16,12 +16,7 @@
package org.springframework.session.hazelcast.config.annotation.web.http;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.annotation.PreDestroy;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.IMap;
@@ -32,11 +27,9 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportAware;
import org.springframework.core.annotation.AnnotationAttributes;
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.MapSession;
import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration;
import org.springframework.session.hazelcast.SessionEntryListener;
import org.springframework.session.hazelcast.HazelcastSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
/**
@@ -45,6 +38,7 @@ import org.springframework.session.web.http.SessionRepositoryFilter;
* {@link HazelcastInstance} must be exposed as a Bean.
*
* @author Tommy Ludwig
* @author Vedran Pavic
* @since 1.1
* @see EnableHazelcastHttpSession
*/
@@ -52,49 +46,30 @@ import org.springframework.session.web.http.SessionRepositoryFilter;
public class HazelcastHttpSessionConfiguration extends SpringHttpSessionConfiguration
implements ImportAware {
private Integer maxInactiveIntervalInSeconds = 1800;
static final String DEFAULT_SESSION_MAP_NAME = "spring:session:sessions";
private String sessionMapName = "spring:session:sessions";
private Integer maxInactiveIntervalInSeconds;
private String sessionListenerUid;
private IMap<String, ExpiringSession> sessionsMap;
private String sessionMapName = DEFAULT_SESSION_MAP_NAME;
@Bean
public SessionRepository<ExpiringSession> sessionRepository(
HazelcastInstance hazelcastInstance, SessionEntryListener sessionListener) {
this.sessionsMap = hazelcastInstance.getMap(this.sessionMapName);
this.sessionListenerUid = this.sessionsMap.addEntryListener(sessionListener,
true);
MapSessionRepository sessionRepository = new MapSessionRepository(
new ExpiringSessionMap(this.sessionsMap));
sessionRepository
.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
return sessionRepository;
}
@PreDestroy
private void removeSessionListener() {
this.sessionsMap.removeEntryListener(this.sessionListenerUid);
}
@Bean
public SessionEntryListener sessionListener(
public HazelcastSessionRepository sessionRepository(
HazelcastInstance hazelcastInstance,
ApplicationEventPublisher eventPublisher) {
return new SessionEntryListener(eventPublisher);
IMap<String, MapSession> sessions = hazelcastInstance.getMap(
this.sessionMapName);
HazelcastSessionRepository sessionRepository = new HazelcastSessionRepository(
sessions);
sessionRepository.setApplicationEventPublisher(eventPublisher);
sessionRepository.setDefaultMaxInactiveInterval(
this.maxInactiveIntervalInSeconds);
return sessionRepository;
}
public void setImportMetadata(AnnotationMetadata importMetadata) {
Map<String, Object> enableAttrMap = importMetadata
.getAnnotationAttributes(EnableHazelcastHttpSession.class.getName());
AnnotationAttributes enableAttrs = AnnotationAttributes.fromMap(enableAttrMap);
transferAnnotationAttributes(enableAttrs);
}
private void transferAnnotationAttributes(AnnotationAttributes enableAttrs) {
setMaxInactiveIntervalInSeconds(
(Integer) enableAttrs.getNumber("maxInactiveIntervalInSeconds"));
setSessionMapName(enableAttrs.getString("sessionMapName"));
@@ -108,74 +83,4 @@ public class HazelcastHttpSessionConfiguration extends SpringHttpSessionConfigur
this.sessionMapName = sessionMapName;
}
/**
* A wrapper for Hazelcast's {@link IMap} which is used to store the sessions.
*/
static class ExpiringSessionMap implements Map<String, ExpiringSession> {
private IMap<String, ExpiringSession> delegate;
ExpiringSessionMap(IMap<String, ExpiringSession> delegate) {
this.delegate = delegate;
}
public ExpiringSession put(String key, ExpiringSession value) {
if (value == null) {
return this.delegate.put(key, value);
}
return this.delegate.put(key, value, value.getMaxInactiveIntervalInSeconds(),
TimeUnit.SECONDS);
}
public int size() {
return this.delegate.size();
}
public boolean isEmpty() {
return this.delegate.isEmpty();
}
public boolean containsKey(Object key) {
return this.delegate.containsKey(key);
}
public boolean containsValue(Object value) {
return this.delegate.containsValue(value);
}
public ExpiringSession get(Object key) {
return this.delegate.get(key);
}
public ExpiringSession remove(Object key) {
return this.delegate.remove(key);
}
public void putAll(Map<? extends String, ? extends ExpiringSession> m) {
this.delegate.putAll(m);
}
public void clear() {
this.delegate.clear();
}
public Set<String> keySet() {
return this.delegate.keySet();
}
public Collection<ExpiringSession> values() {
return this.delegate.values();
}
public Set<java.util.Map.Entry<String, ExpiringSession>> entrySet() {
return this.delegate.entrySet();
}
public boolean equals(Object o) {
return this.delegate.equals(o);
}
public int hashCode() {
return this.delegate.hashCode();
}
}
}

View File

@@ -0,0 +1,232 @@
/*
* Copyright 2014-2016 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.hazelcast;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import com.hazelcast.core.IMap;
import com.hazelcast.query.impl.predicates.EqualPredicate;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.MapSession;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
/**
* Tests for {@link HazelcastSessionRepository}.
*
* @author Vedran Pavic
*/
@RunWith(MockitoJUnitRunner.class)
public class HazelcastSessionRepositoryTests {
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
@Rule
public ExpectedException thrown = ExpectedException.none();
@Mock
private IMap<String, MapSession> sessions;
private HazelcastSessionRepository repository;
@Before
public void setUp() {
this.repository = new HazelcastSessionRepository(this.sessions);
}
@Test
public void constructorNullHazelcastInstance() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Sessions IMap must not be null");
new HazelcastSessionRepository(null);
}
@Test
public void createSessionDefaultMaxInactiveInterval() throws Exception {
MapSession session = this.repository.createSession();
assertThat(session.getMaxInactiveIntervalInSeconds())
.isEqualTo(new MapSession().getMaxInactiveIntervalInSeconds());
verifyZeroInteractions(this.sessions);
}
@Test
public void createSessionCustomMaxInactiveInterval() throws Exception {
int interval = 1;
this.repository.setDefaultMaxInactiveInterval(interval);
MapSession session = this.repository.createSession();
assertThat(session.getMaxInactiveIntervalInSeconds()).isEqualTo(interval);
verifyZeroInteractions(this.sessions);
}
@Test
public void saveNew() {
MapSession session = this.repository.createSession();
this.repository.save(session);
verify(this.sessions, times(1)).put(eq(session.getId()), eq(session),
isA(Long.class), eq(TimeUnit.SECONDS));
}
@Test
public void saveUpdatedAttributes() {
MapSession session = new MapSession();
session.setAttribute("testName", "testValue");
this.repository.save(session);
verify(this.sessions, times(1)).put(eq(session.getId()), eq(session),
isA(Long.class), eq(TimeUnit.SECONDS));
}
@Test
public void saveUpdatedLastAccessedTime() {
MapSession session = new MapSession();
session.setLastAccessedTime(System.currentTimeMillis());
this.repository.save(session);
verify(this.sessions, times(1)).put(eq(session.getId()), eq(session),
isA(Long.class), eq(TimeUnit.SECONDS));
}
@Test
public void saveUnchanged() {
MapSession session = new MapSession();
this.repository.save(session);
verify(this.sessions, times(1)).put(eq(session.getId()), eq(session),
isA(Long.class), eq(TimeUnit.SECONDS));
// TODO - once save optimization is implemented, should be replaced with:
//verifyZeroInteractions(this.sessions);
}
@Test
public void getSessionNotFound() {
String sessionId = "testSessionId";
MapSession session = this.repository.getSession(sessionId);
assertThat(session).isNull();
verify(this.sessions, times(1)).get(eq(sessionId));
}
@Test
public void getSessionExpired() {
MapSession expired = new MapSession();
expired.setLastAccessedTime(System.currentTimeMillis() -
(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS * 1000 + 1000));
given(this.sessions.get(eq(expired.getId()))).willReturn(expired);
MapSession session = this.repository.getSession(expired.getId());
assertThat(session).isNull();
verify(this.sessions, times(1)).get(eq(expired.getId()));
verify(this.sessions, times(1)).remove(eq(expired.getId()));
}
@Test
public void getSessionFound() {
MapSession saved = new MapSession();
saved.setAttribute("savedName", "savedValue");
given(this.sessions.get(eq(saved.getId()))).willReturn(saved);
MapSession session = this.repository.getSession(saved.getId());
assertThat(session.getId()).isEqualTo(saved.getId());
assertThat(session.getAttribute("savedName")).isEqualTo("savedValue");
verify(this.sessions, times(1)).get(eq(saved.getId()));
}
@Test
public void delete() {
String sessionId = "testSessionId";
this.repository.delete(sessionId);
verify(this.sessions, times(1)).remove(eq(sessionId));
}
@Test
public void findByIndexNameAndIndexValueUnknownIndexName() {
String indexValue = "testIndexValue";
Map<String, MapSession> sessions = this.repository.findByIndexNameAndIndexValue(
"testIndexName", indexValue);
assertThat(sessions).isEmpty();
verifyZeroInteractions(this.sessions);
}
@Test
public void findByIndexNameAndIndexValuePrincipalIndexNameNotFound() {
String principal = "username";
Map<String, MapSession> sessions = this.repository.findByIndexNameAndIndexValue(
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principal);
assertThat(sessions).isEmpty();
verify(this.sessions, times(1)).values(isA(EqualPredicate.class));
}
@Test
public void findByIndexNameAndIndexValuePrincipalIndexNameFound() {
String principal = "username";
Authentication authentication = new UsernamePasswordAuthenticationToken(principal,
"notused", AuthorityUtils.createAuthorityList("ROLE_USER"));
List<MapSession> saved = new ArrayList<MapSession>(2);
MapSession saved1 = new MapSession();
saved1.setAttribute(SPRING_SECURITY_CONTEXT, authentication);
saved.add(saved1);
MapSession saved2 = new MapSession();
saved2.setAttribute(SPRING_SECURITY_CONTEXT, authentication);
saved.add(saved2);
given(this.sessions.values(isA(EqualPredicate.class))).willReturn(saved);
Map<String, MapSession> sessions = this.repository.findByIndexNameAndIndexValue(
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principal);
assertThat(sessions).hasSize(2);
verify(this.sessions, times(1)).values(isA(EqualPredicate.class));
}
}

View File

@@ -0,0 +1,197 @@
/*
* Copyright 2014-2016 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.hazelcast.config.annotation.web.http;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.IMap;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.beans.factory.UnsatisfiedDependencyException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.hazelcast.HazelcastSessionRepository;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Matchers.isA;
/**
* Tests for {@link HazelcastHttpSessionConfiguration}.
*
* @author Vedran Pavic
*/
@RunWith(MockitoJUnitRunner.class)
public class HazelcastHttpSessionConfigurationTests {
private static final String MAP_NAME = "spring:test:sessions";
private static final int MAX_INACTIVE_INTERVAL_IN_SECONDS = 600;
@Rule
public final ExpectedException thrown = ExpectedException.none();
@Mock
private static HazelcastInstance hazelcastInstance;
@Mock
private IMap<Object, Object> sessions;
private AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
@Before
public void setUp() {
given(hazelcastInstance.getMap(isA(String.class))).willReturn(this.sessions);
}
@After
public void closeContext() {
if (this.context != null) {
this.context.close();
}
}
@Test
public void noHazelcastInstanceConfiguration() {
this.thrown.expect(UnsatisfiedDependencyException.class);
this.thrown.expectMessage("HazelcastInstance");
registerAndRefresh(EmptyConfiguration.class);
}
@Test
public void defaultConfiguration() {
registerAndRefresh(DefaultConfiguration.class);
assertThat(this.context.getBean(HazelcastSessionRepository.class))
.isNotNull();
}
@Test
public void customTableName() {
registerAndRefresh(CustomSessionMapNameConfiguration.class);
HazelcastSessionRepository repository = this.context
.getBean(HazelcastSessionRepository.class);
HazelcastHttpSessionConfiguration configuration = this.context
.getBean(HazelcastHttpSessionConfiguration.class);
assertThat(repository).isNotNull();
assertThat(ReflectionTestUtils.getField(configuration, "sessionMapName"))
.isEqualTo(MAP_NAME);
}
@Test
public void setCustomSessionMapName() {
registerAndRefresh(BaseConfiguration.class,
CustomSessionMapNameSetConfiguration.class);
HazelcastSessionRepository repository = this.context
.getBean(HazelcastSessionRepository.class);
HazelcastHttpSessionConfiguration configuration = this.context
.getBean(HazelcastHttpSessionConfiguration.class);
assertThat(repository).isNotNull();
assertThat(ReflectionTestUtils.getField(configuration, "sessionMapName"))
.isEqualTo(MAP_NAME);
}
@Test
public void setCustomMaxInactiveIntervalInSeconds() {
registerAndRefresh(BaseConfiguration.class,
CustomMaxInactiveIntervalInSecondsSetConfiguration.class);
HazelcastSessionRepository repository = this.context
.getBean(HazelcastSessionRepository.class);
assertThat(repository).isNotNull();
assertThat(ReflectionTestUtils.getField(repository, "defaultMaxInactiveInterval")).isEqualTo(
MAX_INACTIVE_INTERVAL_IN_SECONDS);
}
@Test
public void customMaxInactiveIntervalInSeconds() {
registerAndRefresh(CustomMaxInactiveIntervalInSecondsConfiguration.class);
HazelcastSessionRepository repository = this.context
.getBean(HazelcastSessionRepository.class);
assertThat(repository).isNotNull();
assertThat(ReflectionTestUtils.getField(repository, "defaultMaxInactiveInterval"))
.isEqualTo(MAX_INACTIVE_INTERVAL_IN_SECONDS);
}
private void registerAndRefresh(Class<?>... annotatedClasses) {
this.context.register(annotatedClasses);
this.context.refresh();
}
@Configuration
@EnableHazelcastHttpSession
static class EmptyConfiguration {
}
static class BaseConfiguration {
@Bean
public HazelcastInstance hazelcastInstance() {
return hazelcastInstance;
}
}
@Configuration
@EnableHazelcastHttpSession
static class DefaultConfiguration extends BaseConfiguration {
}
@Configuration
@EnableHazelcastHttpSession(sessionMapName = MAP_NAME)
static class CustomSessionMapNameConfiguration extends BaseConfiguration {
}
@Configuration
static class CustomSessionMapNameSetConfiguration
extends HazelcastHttpSessionConfiguration {
CustomSessionMapNameSetConfiguration() {
setSessionMapName(MAP_NAME);
}
}
@Configuration
static class CustomMaxInactiveIntervalInSecondsSetConfiguration
extends HazelcastHttpSessionConfiguration {
CustomMaxInactiveIntervalInSecondsSetConfiguration() {
setMaxInactiveIntervalInSeconds(MAX_INACTIVE_INTERVAL_IN_SECONDS);
}
}
@Configuration
@EnableHazelcastHttpSession(maxInactiveIntervalInSeconds = MAX_INACTIVE_INTERVAL_IN_SECONDS)
static class CustomMaxInactiveIntervalInSecondsConfiguration
extends BaseConfiguration {
}
}

View File

@@ -1,11 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<hazelcast
xsi:schemaLocation="http://www.hazelcast.com/schema/config hazelcast-config-3.5.xsd"
xmlns="http://www.hazelcast.com/schema/config" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<hazelcast xmlns="http://www.hazelcast.com/schema/config"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.hazelcast.com/schema/config hazelcast-config-3.6.xsd">
<group>
<name>spring-session-it-test-idle-time-map-name</name>
<password>test-pass</password>
</group>
<network>
<port auto-increment="true" port-count="100">5701</port>
<outbound-ports>
@@ -24,14 +26,20 @@
</aws>
</join>
</network>
<map name="test-sessions">
<in-memory-format>BINARY</in-memory-format>
<backup-count>1</backup-count>
<async-backup-count>0</async-backup-count>
<time-to-live-seconds>0</time-to-live-seconds>
<max-idle-seconds>300</max-idle-seconds>
<merge-policy>com.hazelcast.map.merge.PutIfAbsentMapMergePolicy
</merge-policy>
<merge-policy>com.hazelcast.map.merge.PutIfAbsentMapMergePolicy</merge-policy>
<attributes>
<attribute extractor="org.springframework.session.hazelcast.PrincipalNameExtractor">principalName</attribute>
</attributes>
<indexes>
<index ordered="false">principalName</index>
</indexes>
</map>
</hazelcast>

View File

@@ -1,37 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<hazelcast
xsi:schemaLocation="http://www.hazelcast.com/schema/config hazelcast-config-3.5.xsd"
xmlns="http://www.hazelcast.com/schema/config" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<group>
<name>spring-session-it-test-idle-time</name>
<password>test-pass</password>
</group>
<network>
<port auto-increment="true" port-count="100">5701</port>
<outbound-ports>
<ports>0</ports>
</outbound-ports>
<join>
<multicast enabled="false">
</multicast>
<tcp-ip enabled="true">
<interface>127.0.0.1</interface>
<member-list>
<member>127.0.0.1</member>
</member-list>
</tcp-ip>
<aws enabled="false">
</aws>
</join>
</network>
<map name="spring:session:sessions">
<in-memory-format>BINARY</in-memory-format>
<backup-count>1</backup-count>
<async-backup-count>0</async-backup-count>
<time-to-live-seconds>0</time-to-live-seconds>
<max-idle-seconds>150</max-idle-seconds>
<merge-policy>com.hazelcast.map.merge.PutIfAbsentMapMergePolicy
</merge-policy>
</map>
</hazelcast>

View File

@@ -1,11 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<hazelcast
xsi:schemaLocation="http://www.hazelcast.com/schema/config hazelcast-config-3.5.xsd"
xmlns="http://www.hazelcast.com/schema/config" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<hazelcast xmlns="http://www.hazelcast.com/schema/config"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.hazelcast.com/schema/config hazelcast-config-3.6.xsd">
<group>
<name>spring-session-it-test-map-name</name>
<password>test-pass</password>
</group>
<network>
<port auto-increment="true" port-count="100">5701</port>
<outbound-ports>
@@ -24,14 +26,20 @@
</aws>
</join>
</network>
<map name="my-sessions">
<in-memory-format>BINARY</in-memory-format>
<backup-count>1</backup-count>
<async-backup-count>0</async-backup-count>
<time-to-live-seconds>0</time-to-live-seconds>
<max-idle-seconds>0</max-idle-seconds>
<merge-policy>com.hazelcast.map.merge.PutIfAbsentMapMergePolicy
</merge-policy>
<merge-policy>com.hazelcast.map.merge.PutIfAbsentMapMergePolicy</merge-policy>
<attributes>
<attribute extractor="org.springframework.session.hazelcast.PrincipalNameExtractor">principalName</attribute>
</attributes>
<indexes>
<index ordered="false">principalName</index>
</indexes>
</map>
</hazelcast>