Extract spring-session-data-hazelcast

Issue gh-806
This commit is contained in:
Rob Winch
2017-06-16 15:37:10 -05:00
parent 972cf66d7e
commit c28f047eb5
19 changed files with 10 additions and 4 deletions

View File

@@ -0,0 +1,63 @@
/*
* 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.core.HazelcastInstance;
import com.hazelcast.core.IMap;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.session.MapSession;
import org.springframework.session.hazelcast.HazelcastSessionRepository.HazelcastSession;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Abstract base class for Hazelcast integration tests.
*
* @author Tommy Ludwig
* @author Vedran Pavic
*/
public abstract class AbstractHazelcastRepositoryITests {
@Autowired
private HazelcastInstance hazelcast;
@Autowired
private HazelcastSessionRepository repository;
@Test
public void createAndDestroySession() {
HazelcastSession sessionToSave = this.repository.createSession();
String sessionId = sessionToSave.getId();
IMap<String, MapSession> hazelcastMap = this.hazelcast.getMap(
"spring:session:sessions");
assertThat(hazelcastMap.size()).isEqualTo(0);
this.repository.save(sessionToSave);
assertThat(hazelcastMap.size()).isEqualTo(1);
assertThat(hazelcastMap.get(sessionId)).isEqualTo(sessionToSave);
this.repository.delete(sessionId);
assertThat(hazelcastMap.size()).isEqualTo(0);
}
}

View File

@@ -0,0 +1,76 @@
/*
* 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.client.HazelcastClient;
import com.hazelcast.client.config.ClientConfig;
import com.hazelcast.core.HazelcastInstance;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.runner.RunWith;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.hazelcast.config.annotation.web.http.EnableHazelcastHttpSession;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.util.SocketUtils;
/**
* Integration tests that check the underlying data source - in this case Hazelcast
* Client.
*
* @author Vedran Pavic
* @author Artem Bilan
* @since 1.1
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WebAppConfiguration
public class HazelcastClientRepositoryITests extends AbstractHazelcastRepositoryITests {
private static final int PORT = SocketUtils.findAvailableTcpPort();
private static HazelcastInstance hazelcastInstance;
@BeforeClass
public static void setup() {
hazelcastInstance = HazelcastITestUtils.embeddedHazelcastServer(PORT);
}
@AfterClass
public static void teardown() {
if (hazelcastInstance != null) {
hazelcastInstance.shutdown();
}
}
@Configuration
@EnableHazelcastHttpSession
static class HazelcastSessionConfig {
@Bean
public HazelcastInstance embeddedHazelcastClient() {
ClientConfig clientConfig = new ClientConfig();
clientConfig.getNetworkConfig().addAddress("127.0.0.1:" + PORT);
return HazelcastClient.newHazelcastClient(clientConfig);
}
}
}

View File

@@ -0,0 +1,68 @@
/*
* 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.config.Config;
import com.hazelcast.config.MapAttributeConfig;
import com.hazelcast.config.MapIndexConfig;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import org.springframework.util.SocketUtils;
/**
* Utility class for Hazelcast integration tests.
*
* @author Vedran Pavic
*/
public final class HazelcastITestUtils {
private HazelcastITestUtils() {
}
/**
* Creates {@link HazelcastInstance} for use in integration tests.
* @param port the port for Hazelcast to bind to
* @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.getMapConfig("spring:session:sessions")
.addMapAttributeConfig(attributeConfig)
.addMapIndexConfig(new MapIndexConfig(
HazelcastSessionRepository.PRINCIPAL_NAME_ATTRIBUTE, false));
return Hazelcast.newHazelcastInstance(config);
}
/**
* Creates {@link HazelcastInstance} for use in integration tests.
* @return the Hazelcast instance
*/
public static HazelcastInstance embeddedHazelcastServer() {
return embeddedHazelcastServer(SocketUtils.findAvailableTcpPort());
}
}

View File

@@ -0,0 +1,52 @@
/*
* 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.core.HazelcastInstance;
import org.junit.runner.RunWith;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.hazelcast.config.annotation.web.http.EnableHazelcastHttpSession;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
/**
* Integration tests that check the underlying data source - in this case Hazelcast
* Server.
*
* @author Tommy Ludwig
* @author Vedran Pavic
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WebAppConfiguration
public class HazelcastServerRepositoryITests extends AbstractHazelcastRepositoryITests {
@EnableHazelcastHttpSession
@Configuration
static class HazelcastSessionConfig {
@Bean
public HazelcastInstance embeddedHazelcastServer() {
return HazelcastITestUtils.embeddedHazelcastServer();
}
}
}

View File

@@ -0,0 +1,186 @@
/*
* Copyright 2014-2017 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 java.time.Duration;
import java.time.Instant;
import com.hazelcast.core.HazelcastInstance;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.data.SessionEventRegistry;
import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDeletedEvent;
import org.springframework.session.events.SessionExpiredEvent;
import org.springframework.session.hazelcast.HazelcastITestUtils;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Ensure that the appropriate SessionEvents are fired at the expected times. Additionally
* ensure that the interactions with the {@link SessionRepository} abstraction behave as
* expected after each SessionEvent.
*
* @author Tommy Ludwig
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WebAppConfiguration
public class EnableHazelcastHttpSessionEventsTests<S extends Session> {
private final static int MAX_INACTIVE_INTERVAL_IN_SECONDS = 1;
@Autowired
private SessionRepository<S> repository;
@Autowired
private SessionEventRegistry registry;
@Before
public void setup() {
this.registry.clear();
}
@Test
public void saveSessionTest() throws InterruptedException {
String username = "saves-" + System.currentTimeMillis();
S sessionToSave = this.repository.createSession();
String expectedAttributeName = "a";
String expectedAttributeValue = "b";
sessionToSave.setAttribute(expectedAttributeName, expectedAttributeValue);
Authentication toSaveToken = new UsernamePasswordAuthenticationToken(username,
"password", AuthorityUtils.createAuthorityList("ROLE_USER"));
SecurityContext toSaveContext = SecurityContextHolder.createEmptyContext();
toSaveContext.setAuthentication(toSaveToken);
sessionToSave.setAttribute("SPRING_SECURITY_CONTEXT", toSaveContext);
sessionToSave.setAttribute(
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, username);
this.repository.save(sessionToSave);
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isTrue();
assertThat(this.registry.<SessionCreatedEvent>getEvent(sessionToSave.getId()))
.isInstanceOf(SessionCreatedEvent.class);
Session session = this.repository.getSession(sessionToSave.getId());
assertThat(session.getId()).isEqualTo(sessionToSave.getId());
assertThat(session.getAttributeNames())
.isEqualTo(sessionToSave.getAttributeNames());
assertThat(session.<String>getAttribute(expectedAttributeName))
.isEqualTo(sessionToSave.getAttribute(expectedAttributeName));
}
@Test
public void expiredSessionTest() throws InterruptedException {
S sessionToSave = this.repository.createSession();
this.repository.save(sessionToSave);
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isTrue();
assertThat(this.registry.<SessionCreatedEvent>getEvent(sessionToSave.getId()))
.isInstanceOf(SessionCreatedEvent.class);
this.registry.clear();
assertThat(sessionToSave.getMaxInactiveInterval())
.isEqualTo(Duration.ofSeconds(MAX_INACTIVE_INTERVAL_IN_SECONDS));
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isTrue();
assertThat(this.registry.<SessionExpiredEvent>getEvent(sessionToSave.getId()))
.isInstanceOf(SessionExpiredEvent.class);
assertThat(this.repository.<Session>getSession(sessionToSave.getId())).isNull();
}
@Test
public void deletedSessionTest() throws InterruptedException {
S sessionToSave = this.repository.createSession();
this.repository.save(sessionToSave);
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isTrue();
assertThat(this.registry.<SessionCreatedEvent>getEvent(sessionToSave.getId()))
.isInstanceOf(SessionCreatedEvent.class);
this.registry.clear();
this.repository.delete(sessionToSave.getId());
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isTrue();
assertThat(this.registry.<SessionDeletedEvent>getEvent(sessionToSave.getId()))
.isInstanceOf(SessionDeletedEvent.class);
assertThat(this.repository.getSession(sessionToSave.getId())).isNull();
}
@Test
public void saveUpdatesTimeToLiveTest() throws InterruptedException {
Object lock = new Object();
S sessionToSave = this.repository.createSession();
this.repository.save(sessionToSave);
synchronized (lock) {
lock.wait(sessionToSave.getMaxInactiveInterval().minusMillis(500).toMillis());
}
// Get and save the session like SessionRepositoryFilter would.
S sessionToUpdate = this.repository.getSession(sessionToSave.getId());
sessionToUpdate.setLastAccessedTime(Instant.now());
this.repository.save(sessionToUpdate);
synchronized (lock) {
lock.wait(sessionToUpdate.getMaxInactiveInterval().minusMillis(100).toMillis());
}
assertThat(this.repository.getSession(sessionToUpdate.getId())).isNotNull();
}
@Configuration
@EnableHazelcastHttpSession(maxInactiveIntervalInSeconds = MAX_INACTIVE_INTERVAL_IN_SECONDS)
static class HazelcastSessionConfig {
@Bean
public HazelcastInstance embeddedHazelcast() {
return HazelcastITestUtils.embeddedHazelcastServer();
}
@Bean
public SessionEventRegistry sessionEventRegistry() {
return new SessionEventRegistry();
}
}
}

View File

@@ -0,0 +1,125 @@
/*
* Copyright 2014-2017 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 java.time.Duration;
import com.hazelcast.config.ClasspathXmlConfig;
import com.hazelcast.config.Config;
import com.hazelcast.config.NetworkConfig;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.util.SocketUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Test the different configuration options for the {@link EnableHazelcastHttpSession}
* annotation.
*
* @author Tommy Ludwig
*/
public class HazelcastHttpSessionConfigurationXmlTests<S extends Session> {
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WebAppConfiguration
public static class CustomXmlMapNameTest<S extends Session> {
@Autowired
private SessionRepository<S> repository;
@Test
public void saveSessionTest() throws InterruptedException {
S sessionToSave = this.repository.createSession();
this.repository.save(sessionToSave);
S session = this.repository.getSession(sessionToSave.getId());
assertThat(session.getId()).isEqualTo(sessionToSave.getId());
assertThat(session.getMaxInactiveInterval())
.isEqualTo(Duration.ofMinutes(30));
}
@Configuration
@EnableHazelcastHttpSession(sessionMapName = "my-sessions")
static class HazelcastSessionXmlConfigCustomMapName {
@Bean
public HazelcastInstance embeddedHazelcast() {
Config hazelcastConfig = new ClasspathXmlConfig(
"org/springframework/session/hazelcast/config/annotation/web/http/hazelcast-custom-map-name.xml");
NetworkConfig netConfig = new NetworkConfig();
netConfig.setPort(SocketUtils.findAvailableTcpPort());
hazelcastConfig.setNetworkConfig(netConfig);
return Hazelcast.newHazelcastInstance(hazelcastConfig);
}
}
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WebAppConfiguration
public static class CustomXmlMapNameAndIdleTest<S extends Session> {
@Autowired
private SessionRepository<S> repository;
@Test
public void saveSessionTest() throws InterruptedException {
S sessionToSave = this.repository.createSession();
this.repository.save(sessionToSave);
S session = this.repository.getSession(sessionToSave.getId());
assertThat(session.getId()).isEqualTo(sessionToSave.getId());
assertThat(session.getMaxInactiveInterval())
.isEqualTo(Duration.ofMinutes(20));
}
@Configuration
@EnableHazelcastHttpSession(sessionMapName = "test-sessions", maxInactiveIntervalInSeconds = 1200)
static class HazelcastSessionXmlConfigCustomMapNameAndIdle {
@Bean
public HazelcastInstance embeddedHazelcast() {
Config hazelcastConfig = new ClasspathXmlConfig(
"org/springframework/session/hazelcast/config/annotation/web/http/hazelcast-custom-idle-time-map-name.xml");
NetworkConfig netConfig = new NetworkConfig();
netConfig.setPort(SocketUtils.findAvailableTcpPort());
hazelcastConfig.setNetworkConfig(netConfig);
return Hazelcast.newHazelcastInstance(hazelcastConfig);
}
}
}
}

View File

@@ -0,0 +1,44 @@
/*
* 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 org.springframework.session.SessionRepository;
/**
* Specifies when to write to the backing Hazelcast instance.
*
* @author Aleksandar Stojsavljevic
* @since 1.3.0
*/
public enum HazelcastFlushMode {
/**
* Only writes to Hazelcast when
* {@link SessionRepository#save(org.springframework.session.Session)} is invoked. In
* a web environment this is typically done as soon as the HTTP response is committed.
*/
ON_SAVE,
/**
* Writes to Hazelcast as soon as possible. For example
* {@link SessionRepository#createSession()} will write the session to Hazelcast.
* Another example is that setting an attribute on the session will also write to
* Hazelcast immediately.
*/
IMMEDIATE
}

View File

@@ -0,0 +1,370 @@
/*
* Copyright 2014-2017 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.time.Duration;
import java.time.Instant;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
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.Session;
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
* @author Aleksandar Stojsavljevic
* @since 1.3.0
*/
public class HazelcastSessionRepository implements
FindByIndexNameSessionRepository<HazelcastSessionRepository.HazelcastSession>,
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 HazelcastFlushMode hazelcastFlushMode = HazelcastFlushMode.ON_SAVE;
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#setMaxInactiveInterval(Duration)}.
*/
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;
}
/**
* Sets the Hazelcast flush mode. Default flush mode is
* {@link HazelcastFlushMode#ON_SAVE}.
* @param hazelcastFlushMode the new Hazelcast flush mode
*/
public void setHazelcastFlushMode(HazelcastFlushMode hazelcastFlushMode) {
Assert.notNull(hazelcastFlushMode, "HazelcastFlushMode cannot be null");
this.hazelcastFlushMode = hazelcastFlushMode;
}
public HazelcastSession createSession() {
HazelcastSession result = new HazelcastSession();
if (this.defaultMaxInactiveInterval != null) {
result.setMaxInactiveInterval(
Duration.ofSeconds(this.defaultMaxInactiveInterval));
}
return result;
}
public void save(HazelcastSession session) {
if (session.isChanged()) {
this.sessions.put(session.getId(), session.getDelegate(),
session.getMaxInactiveInterval().getSeconds(), TimeUnit.SECONDS);
session.markUnchanged();
}
}
public HazelcastSession getSession(String id) {
MapSession saved = this.sessions.get(id);
if (saved == null) {
return null;
}
if (saved.isExpired()) {
delete(saved.getId());
return null;
}
return new HazelcastSession(saved);
}
public void delete(String id) {
this.sessions.remove(id);
}
public Map<String, HazelcastSession> 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, HazelcastSession> sessionMap = new HashMap<>(
sessions.size());
for (MapSession session : sessions) {
sessionMap.put(session.getId(), new HazelcastSession(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()));
}
/**
* A custom implementation of {@link Session} that uses a {@link MapSession} as the
* basis for its mapping. It keeps track if changes have been made since last save.
*
* @author Aleksandar Stojsavljevic
*/
final class HazelcastSession implements Session {
private final MapSession delegate;
private boolean changed;
/**
* Creates a new instance ensuring to mark all of the new attributes to be
* persisted in the next save operation.
*/
HazelcastSession() {
this(new MapSession());
this.changed = true;
flushImmediateIfNecessary();
}
/**
* Creates a new instance from the provided {@link MapSession}.
* @param cached the {@link MapSession} that represents the persisted session that
* was retrieved. Cannot be {@code null}.
*/
HazelcastSession(MapSession cached) {
Assert.notNull(cached, "MapSession cannot be null");
this.delegate = cached;
}
public void setLastAccessedTime(Instant lastAccessedTime) {
this.delegate.setLastAccessedTime(lastAccessedTime);
this.changed = true;
flushImmediateIfNecessary();
}
public boolean isExpired() {
return this.delegate.isExpired();
}
public Instant getCreationTime() {
return this.delegate.getCreationTime();
}
public String getId() {
return this.delegate.getId();
}
public Instant getLastAccessedTime() {
return this.delegate.getLastAccessedTime();
}
public void setMaxInactiveInterval(Duration interval) {
this.delegate.setMaxInactiveInterval(interval);
this.changed = true;
flushImmediateIfNecessary();
}
public Duration getMaxInactiveInterval() {
return this.delegate.getMaxInactiveInterval();
}
public <T> Optional<T> getAttribute(String attributeName) {
return this.delegate.getAttribute(attributeName);
}
public Set<String> getAttributeNames() {
return this.delegate.getAttributeNames();
}
public void setAttribute(String attributeName, Object attributeValue) {
this.delegate.setAttribute(attributeName, attributeValue);
this.changed = true;
flushImmediateIfNecessary();
}
public void removeAttribute(String attributeName) {
this.delegate.removeAttribute(attributeName);
this.changed = true;
flushImmediateIfNecessary();
}
boolean isChanged() {
return this.changed;
}
void markUnchanged() {
this.changed = false;
}
MapSession getDelegate() {
return this.delegate;
}
private void flushImmediateIfNecessary() {
if (HazelcastSessionRepository.this.hazelcastFlushMode == HazelcastFlushMode.IMMEDIATE) {
HazelcastSessionRepository.this.save(this);
}
}
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2014-2017 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.Optional;
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) {
Optional<String> principalName = session.getAttribute(
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
if (principalName.isPresent()) {
return principalName.get();
}
Optional<Object> authentication = session.getAttribute(SPRING_SECURITY_CONTEXT);
if (authentication.isPresent()) {
Expression expression = this.parser
.parseExpression("authentication?.name");
return expression.getValue(authentication.get(), String.class);
}
return null;
}
}
}

View File

@@ -0,0 +1,92 @@
/*
* 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 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.MapSession;
import org.springframework.session.SessionRepository;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
import org.springframework.session.hazelcast.HazelcastFlushMode;
/**
* 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 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
* @author Aleksandar Stojsavljevic
* @since 1.1
* @see EnableSpringHttpSession
*/
@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@Target({ 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.
*
* @return the seconds a session can be inactive before expiring
*/
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 {@link HazelcastHttpSessionConfiguration#DEFAULT_SESSION_MAP_NAME}.
* @return the name of the Map to store the sessions in Hazelcast
*/
String sessionMapName() default HazelcastHttpSessionConfiguration.DEFAULT_SESSION_MAP_NAME;
/**
* Flush mode for the Hazelcast sessions. The default is {@code ON_SAVE} which only
* updates the backing Hazelcast when
* {@link SessionRepository#save(org.springframework.session.Session)} is invoked. In
* a web environment this happens just before the HTTP response is committed.
* <p>
* Setting the value to {@code IMMEDIATE} will ensure that the any updates to the
* Session are immediately written to the Hazelcast instance.
* @return the {@link HazelcastFlushMode} to use
* @since 1.3.0
*/
HazelcastFlushMode hazelcastFlushMode() default HazelcastFlushMode.ON_SAVE;
}

View File

@@ -0,0 +1,96 @@
/*
* 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 java.util.Map;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.IMap;
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.type.AnnotationMetadata;
import org.springframework.session.MapSession;
import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration;
import org.springframework.session.hazelcast.HazelcastFlushMode;
import org.springframework.session.hazelcast.HazelcastSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
/**
* 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
* @author Vedran Pavic
* @since 1.1
* @see EnableHazelcastHttpSession
*/
@Configuration
public class HazelcastHttpSessionConfiguration extends SpringHttpSessionConfiguration
implements ImportAware {
static final String DEFAULT_SESSION_MAP_NAME = "spring:session:sessions";
private Integer maxInactiveIntervalInSeconds;
private String sessionMapName = DEFAULT_SESSION_MAP_NAME;
private HazelcastFlushMode hazelcastFlushMode = HazelcastFlushMode.ON_SAVE;
@Bean
public HazelcastSessionRepository sessionRepository(
HazelcastInstance hazelcastInstance,
ApplicationEventPublisher eventPublisher) {
IMap<String, MapSession> sessions = hazelcastInstance.getMap(
this.sessionMapName);
HazelcastSessionRepository sessionRepository = new HazelcastSessionRepository(
sessions);
sessionRepository.setApplicationEventPublisher(eventPublisher);
sessionRepository.setDefaultMaxInactiveInterval(
this.maxInactiveIntervalInSeconds);
sessionRepository.setHazelcastFlushMode(this.hazelcastFlushMode);
return sessionRepository;
}
public void setImportMetadata(AnnotationMetadata importMetadata) {
Map<String, Object> enableAttrMap = importMetadata
.getAnnotationAttributes(EnableHazelcastHttpSession.class.getName());
AnnotationAttributes enableAttrs = AnnotationAttributes.fromMap(enableAttrMap);
setMaxInactiveIntervalInSeconds(
(Integer) enableAttrs.getNumber("maxInactiveIntervalInSeconds"));
setSessionMapName(enableAttrs.getString("sessionMapName"));
setHazelcastFlushMode(
(HazelcastFlushMode) enableAttrs.getEnum("hazelcastFlushMode"));
}
public void setMaxInactiveIntervalInSeconds(int maxInactiveIntervalInSeconds) {
this.maxInactiveIntervalInSeconds = maxInactiveIntervalInSeconds;
}
public void setSessionMapName(String sessionMapName) {
this.sessionMapName = sessionMapName;
}
public void setHazelcastFlushMode(HazelcastFlushMode hazelcastFlushMode) {
this.hazelcastFlushMode = hazelcastFlushMode;
}
}

View File

@@ -0,0 +1,331 @@
/*
* Copyright 2014-2017 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.time.Duration;
import java.time.Instant;
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.junit.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 org.springframework.session.hazelcast.HazelcastSessionRepository.HazelcastSession;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.BDDMockito.given;
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
* @author Aleksandar Stojsavljevic
*/
@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 {
HazelcastSession session = this.repository.createSession();
assertThat(session.getMaxInactiveInterval())
.isEqualTo(new MapSession().getMaxInactiveInterval());
verifyZeroInteractions(this.sessions);
}
@Test
public void createSessionCustomMaxInactiveInterval() throws Exception {
int interval = 1;
this.repository.setDefaultMaxInactiveInterval(interval);
HazelcastSession session = this.repository.createSession();
assertThat(session.getMaxInactiveInterval())
.isEqualTo(Duration.ofSeconds(interval));
verifyZeroInteractions(this.sessions);
}
@Test
public void saveNewFlushModeOnSave() {
HazelcastSession session = this.repository.createSession();
verifyZeroInteractions(this.sessions);
this.repository.save(session);
verify(this.sessions, times(1)).put(eq(session.getId()), eq(session.getDelegate()),
isA(Long.class), eq(TimeUnit.SECONDS));
}
@Test
public void saveNewFlushModeImmediate() {
this.repository.setHazelcastFlushMode(HazelcastFlushMode.IMMEDIATE);
HazelcastSession session = this.repository.createSession();
verify(this.sessions, times(1)).put(eq(session.getId()), eq(session.getDelegate()),
isA(Long.class), eq(TimeUnit.SECONDS));
}
@Test
public void saveUpdatedAttributeFlushModeOnSave() {
HazelcastSession session = this.repository.createSession();
session.setAttribute("testName", "testValue");
verifyZeroInteractions(this.sessions);
this.repository.save(session);
verify(this.sessions, times(1)).put(eq(session.getId()), eq(session.getDelegate()),
isA(Long.class), eq(TimeUnit.SECONDS));
}
@Test
public void saveUpdatedAttributeFlushModeImmediate() {
this.repository.setHazelcastFlushMode(HazelcastFlushMode.IMMEDIATE);
HazelcastSession session = this.repository.createSession();
session.setAttribute("testName", "testValue");
verify(this.sessions, times(2)).put(eq(session.getId()), eq(session.getDelegate()),
isA(Long.class), eq(TimeUnit.SECONDS));
this.repository.save(session);
verifyZeroInteractions(this.sessions);
}
@Test
public void removeAttributeFlushModeOnSave() {
HazelcastSession session = this.repository.createSession();
session.removeAttribute("testName");
verifyZeroInteractions(this.sessions);
this.repository.save(session);
verify(this.sessions, times(1)).put(eq(session.getId()), eq(session.getDelegate()),
isA(Long.class), eq(TimeUnit.SECONDS));
}
@Test
public void removeAttributeFlushModeImmediate() {
this.repository.setHazelcastFlushMode(HazelcastFlushMode.IMMEDIATE);
HazelcastSession session = this.repository.createSession();
session.removeAttribute("testName");
verify(this.sessions, times(2)).put(eq(session.getId()), eq(session.getDelegate()),
isA(Long.class), eq(TimeUnit.SECONDS));
this.repository.save(session);
verifyZeroInteractions(this.sessions);
}
@Test
public void saveUpdatedLastAccessedTimeFlushModeOnSave() {
HazelcastSession session = this.repository.createSession();
session.setLastAccessedTime(Instant.now());
verifyZeroInteractions(this.sessions);
this.repository.save(session);
verify(this.sessions, times(1)).put(eq(session.getId()), eq(session.getDelegate()),
isA(Long.class), eq(TimeUnit.SECONDS));
}
@Test
public void saveUpdatedLastAccessedTimeFlushModeImmediate() {
this.repository.setHazelcastFlushMode(HazelcastFlushMode.IMMEDIATE);
HazelcastSession session = this.repository.createSession();
session.setLastAccessedTime(Instant.now());
verify(this.sessions, times(2)).put(eq(session.getId()), eq(session.getDelegate()),
isA(Long.class), eq(TimeUnit.SECONDS));
this.repository.save(session);
verifyZeroInteractions(this.sessions);
}
@Test
public void saveUpdatedMaxInactiveIntervalInSecondsFlushModeOnSave() {
HazelcastSession session = this.repository.createSession();
session.setMaxInactiveInterval(Duration.ofSeconds(1));
verifyZeroInteractions(this.sessions);
this.repository.save(session);
verify(this.sessions, times(1)).put(eq(session.getId()), eq(session.getDelegate()),
isA(Long.class), eq(TimeUnit.SECONDS));
}
@Test
public void saveUpdatedMaxInactiveIntervalInSecondsFlushModeImmediate() {
this.repository.setHazelcastFlushMode(HazelcastFlushMode.IMMEDIATE);
HazelcastSession session = this.repository.createSession();
session.setMaxInactiveInterval(Duration.ofSeconds(1));
verify(this.sessions, times(2)).put(eq(session.getId()), eq(session.getDelegate()),
isA(Long.class), eq(TimeUnit.SECONDS));
this.repository.save(session);
verifyZeroInteractions(this.sessions);
}
@Test
public void saveUnchangedFlushModeOnSave() {
HazelcastSession session = this.repository.createSession();
this.repository.save(session);
verify(this.sessions, times(1)).put(eq(session.getId()), eq(session.getDelegate()),
isA(Long.class), eq(TimeUnit.SECONDS));
this.repository.save(session);
verifyZeroInteractions(this.sessions);
}
@Test
public void saveUnchangedFlushModeImmediate() {
this.repository.setHazelcastFlushMode(HazelcastFlushMode.IMMEDIATE);
HazelcastSession session = this.repository.createSession();
verify(this.sessions, times(1)).put(eq(session.getId()), eq(session.getDelegate()),
isA(Long.class), eq(TimeUnit.SECONDS));
this.repository.save(session);
verifyZeroInteractions(this.sessions);
}
@Test
public void getSessionNotFound() {
String sessionId = "testSessionId";
HazelcastSession 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(Instant.now().minusSeconds(
MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS + 1));
given(this.sessions.get(eq(expired.getId()))).willReturn(expired);
HazelcastSession 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);
HazelcastSession session = this.repository.getSession(saved.getId());
assertThat(session.getId()).isEqualTo(saved.getId());
assertThat(session.<String>getAttribute("savedName").orElse(null)).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, HazelcastSession> sessions = this.repository.findByIndexNameAndIndexValue(
"testIndexName", indexValue);
assertThat(sessions).isEmpty();
verifyZeroInteractions(this.sessions);
}
@Test
public void findByIndexNameAndIndexValuePrincipalIndexNameNotFound() {
String principal = "username";
Map<String, HazelcastSession> 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<>(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, HazelcastSession> 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,240 @@
/*
* Copyright 2014-2017 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.junit.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.HazelcastFlushMode;
import org.springframework.session.hazelcast.HazelcastSessionRepository;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.BDDMockito.given;
/**
* Tests for {@link HazelcastHttpSessionConfiguration}.
*
* @author Vedran Pavic
* @author Aleksandar Stojsavljevic
*/
@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;
private static final HazelcastFlushMode HAZELCAST_FLUSH_MODE = HazelcastFlushMode.IMMEDIATE;
@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);
}
@Test
public void customFlushImmediately() {
registerAndRefresh(CustomFlushImmediatelyConfiguration.class);
HazelcastSessionRepository repository = this.context
.getBean(HazelcastSessionRepository.class);
assertThat(repository).isNotNull();
assertThat(ReflectionTestUtils.getField(repository, "hazelcastFlushMode"))
.isEqualTo(HazelcastFlushMode.IMMEDIATE);
}
@Test
public void setCustomFlushImmediately() {
registerAndRefresh(BaseConfiguration.class,
CustomFlushImmediatelySetConfiguration.class);
HazelcastSessionRepository repository = this.context
.getBean(HazelcastSessionRepository.class);
assertThat(repository).isNotNull();
assertThat(ReflectionTestUtils.getField(repository, "hazelcastFlushMode"))
.isEqualTo(HazelcastFlushMode.IMMEDIATE);
}
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 {
}
@Configuration
static class CustomFlushImmediatelySetConfiguration
extends HazelcastHttpSessionConfiguration {
CustomFlushImmediatelySetConfiguration() {
setHazelcastFlushMode(HAZELCAST_FLUSH_MODE);
}
}
@Configuration
@EnableHazelcastHttpSession(hazelcastFlushMode = HazelcastFlushMode.IMMEDIATE)
static class CustomFlushImmediatelyConfiguration
extends BaseConfiguration {
}
}

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<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>
<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="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>
<attributes>
<attribute extractor="org.springframework.session.hazelcast.PrincipalNameExtractor">principalName</attribute>
</attributes>
<indexes>
<index ordered="false">principalName</index>
</indexes>
</map>
</hazelcast>

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<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>
<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="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>
<attributes>
<attribute extractor="org.springframework.session.hazelcast.PrincipalNameExtractor">principalName</attribute>
</attributes>
<indexes>
<index ordered="false">principalName</index>
</indexes>
</map>
</hazelcast>