diff --git a/spring-session/src/main/java/org/springframework/session/SafeRetrievingSessionRepository.java b/spring-session/src/main/java/org/springframework/session/SafeRetrievingSessionRepository.java
new file mode 100644
index 000000000..35a35f88f
--- /dev/null
+++ b/spring-session/src/main/java/org/springframework/session/SafeRetrievingSessionRepository.java
@@ -0,0 +1,185 @@
+/*
+ * 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;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.util.Assert;
+
+/**
+ * A {@link FindByIndexNameSessionRepository} that decorates the delegating
+ * {@link SessionRepository}, ignoring the configured set of exceptions during retrieval
+ * of a {@link Session} from the underlying store. Useful with handling exceptions
+ * during session deserialization (for example, when serialization UID changes) in
+ * scenarios where {@link SessionRepository} consumer wants to treat the session that
+ * wasn't deserializable as non-existing.
+ *
+ * By default, this implementation will delete the session whose retrieval using
+ * {@link #getSession(String)} has failed with the configured ignored exception. This
+ * behavior can be changed using {@link #setDeleteOnIgnoredException(boolean)} method.
+ *
+ * @param the type of {@link Session} being managed
+ * @author Vedran Pavic
+ * @since 1.3.0
+ */
+public class SafeRetrievingSessionRepository
+ implements FindByIndexNameSessionRepository {
+
+ private static final Log logger = LogFactory.getLog(SafeRetrievingSessionRepository.class);
+
+ private final FindByIndexNameSessionRepository delegate;
+
+ private final Set> ignoredExceptions;
+
+ private boolean deleteOnIgnoredException = true;
+
+ private boolean logIgnoredException;
+
+ /**
+ * Create a new {@link SafeRetrievingSessionRepository} instance backed by a
+ * {@link FindByIndexNameSessionRepository} delegate.
+ * @param delegate the {@link FindByIndexNameSessionRepository} delegate
+ * @param ignoredExceptions the set of exceptions to ignore
+ */
+ public SafeRetrievingSessionRepository(
+ FindByIndexNameSessionRepository delegate,
+ Set> ignoredExceptions) {
+ Assert.notNull(delegate, "Delegate must not be null");
+ Assert.notEmpty(ignoredExceptions, "Ignored exceptions must not be empty");
+ this.delegate = delegate;
+ this.ignoredExceptions = ignoredExceptions;
+ }
+
+ /**
+ * Create a new {@link SafeRetrievingSessionRepository} instance backed by a
+ * {@link SessionRepository} delegate.
+ * @param delegate the {@link SessionRepository} delegate
+ * @param ignoredExceptions the set of exceptions to ignore
+ */
+ public SafeRetrievingSessionRepository(SessionRepository delegate,
+ Set> ignoredExceptions) {
+ this(new FindByIndexNameSessionRepositoryAdapter(delegate), ignoredExceptions);
+ }
+
+ /**
+ * Set whether session should be deleted after ignored exception has occurred during
+ * retrieval.
+ * @param deleteOnIgnoredException the flag to indicate whether session should be
+ * deleted
+ */
+ public void setDeleteOnIgnoredException(boolean deleteOnIgnoredException) {
+ this.deleteOnIgnoredException = deleteOnIgnoredException;
+ }
+
+ /**
+ * Set whether ignored exception should be logged.
+ * @param logIgnoredException the flag to indicate whether to log ignored exceptions
+ */
+ public void setLogIgnoredException(boolean logIgnoredException) {
+ this.logIgnoredException = logIgnoredException;
+ }
+
+ public S createSession() {
+ return this.delegate.createSession();
+ }
+
+ public void save(S session) {
+ this.delegate.save(session);
+ }
+
+ public S getSession(String id) {
+ try {
+ return this.delegate.getSession(id);
+ }
+ catch (RuntimeException e) {
+ if (isIgnoredException(e)) {
+ if (this.logIgnoredException) {
+ logger.warn("Error occurred while retrieving session " + id + ": "
+ + e);
+ }
+ if (this.deleteOnIgnoredException) {
+ this.delegate.delete(id);
+ }
+ return null;
+ }
+ throw e;
+ }
+ }
+
+ public void delete(String id) {
+ this.delegate.delete(id);
+ }
+
+ public Map findByIndexNameAndIndexValue(String indexName, String indexValue) {
+ try {
+ return this.delegate.findByIndexNameAndIndexValue(indexName, indexValue);
+ }
+ catch (RuntimeException e) {
+ if (this.logIgnoredException) {
+ logger.warn("Error occurred while retrieving sessions with index name '"
+ + indexName + "' and value '" + indexValue + "': " + e);
+ }
+ if (isIgnoredException(e)) {
+ return Collections.emptyMap();
+ }
+ throw e;
+ }
+ }
+
+ public boolean isIgnoredException(RuntimeException e) {
+ return this.ignoredExceptions.contains(e.getClass());
+ }
+
+ private static class FindByIndexNameSessionRepositoryAdapter
+ implements FindByIndexNameSessionRepository {
+
+ private final SessionRepository delegate;
+
+ FindByIndexNameSessionRepositoryAdapter(SessionRepository delegate) {
+ Assert.notNull(delegate, "Delegate must not be null");
+ this.delegate = delegate;
+ }
+
+ public S createSession() {
+ return this.delegate.createSession();
+ }
+
+ public void save(S session) {
+ this.delegate.save(session);
+ }
+
+ public S getSession(String id) {
+ return this.delegate.getSession(id);
+ }
+
+ public void delete(String id) {
+ this.delegate.delete(id);
+ }
+
+ public Map findByIndexNameAndIndexValue(String indexName,
+ String indexValue) {
+ throw new UnsupportedOperationException();
+ }
+
+ }
+
+}
diff --git a/spring-session/src/test/java/org/springframework/session/SafeRetrievingSessionRepositoryTests.java b/spring-session/src/test/java/org/springframework/session/SafeRetrievingSessionRepositoryTests.java
new file mode 100644
index 000000000..ee3fbf86e
--- /dev/null
+++ b/spring-session/src/test/java/org/springframework/session/SafeRetrievingSessionRepositoryTests.java
@@ -0,0 +1,213 @@
+/*
+ * 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;
+
+import java.util.Collections;
+import java.util.UUID;
+
+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 static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+/**
+ * Tests for {@link SafeRetrievingSessionRepository}.
+ *
+ * @author Vedran Pavic
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class SafeRetrievingSessionRepositoryTests {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Mock
+ private FindByIndexNameSessionRepository delegate;
+
+ @Test
+ public void createWithNullDelegateFails() {
+ this.thrown.expect(IllegalArgumentException.class);
+ this.thrown.expectMessage("Delegate must not be null");
+ new SafeRetrievingSessionRepository(null,
+ Collections.>singleton(RuntimeException.class));
+ }
+
+ @Test
+ public void createWithNullIgnoredExceptionsFails() {
+ this.thrown.expect(IllegalArgumentException.class);
+ this.thrown.expectMessage("Ignored exceptions must not be empty");
+ new SafeRetrievingSessionRepository(this.delegate, null);
+ }
+
+ @Test
+ public void createWithEmptyIgnoredExceptionsFails() {
+ this.thrown.expect(IllegalArgumentException.class);
+ this.thrown.expectMessage("Ignored exceptions must not be empty");
+ new SafeRetrievingSessionRepository(this.delegate,
+ Collections.>emptySet());
+ }
+
+ @Test
+ public void createSession() {
+ SafeRetrievingSessionRepository repository =
+ new SafeRetrievingSessionRepository(this.delegate,
+ Collections.>singleton(RuntimeException.class));
+ repository.createSession();
+ verify(this.delegate, times(1)).createSession();
+ verifyZeroInteractions(this.delegate);
+ }
+
+ @Test
+ public void saveSession() {
+ SafeRetrievingSessionRepository repository =
+ new SafeRetrievingSessionRepository(this.delegate,
+ Collections.>singleton(RuntimeException.class));
+ repository.save(new MapSession());
+ verify(this.delegate, times(1)).save(any(ExpiringSession.class));
+ verifyZeroInteractions(this.delegate);
+ }
+
+ @Test
+ public void getSession() {
+ ExpiringSession session = mock(ExpiringSession.class);
+ given(this.delegate.getSession(anyString())).willReturn(session);
+ SafeRetrievingSessionRepository repository =
+ new SafeRetrievingSessionRepository(this.delegate,
+ Collections.>singleton(RuntimeException.class));
+ assertThat(repository.getSession(UUID.randomUUID().toString()))
+ .isEqualTo(session);
+ verify(this.delegate, times(1)).getSession(anyString());
+ verifyZeroInteractions(this.delegate);
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void getSessionThrowsIgnoredException() {
+ given(this.delegate.getSession(anyString())).willThrow(RuntimeException.class);
+ SafeRetrievingSessionRepository repository =
+ new SafeRetrievingSessionRepository(this.delegate,
+ Collections.>singleton(RuntimeException.class));
+ assertThat(repository.getSession(UUID.randomUUID().toString())).isNull();
+ verify(this.delegate, times(1)).getSession(anyString());
+ verify(this.delegate, times(1)).delete(anyString());
+ verifyZeroInteractions(this.delegate);
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void getSessionThrowsIgnoredExceptionWithDeletionDisabled() {
+ given(this.delegate.getSession(anyString())).willThrow(RuntimeException.class);
+ SafeRetrievingSessionRepository repository =
+ new SafeRetrievingSessionRepository(this.delegate,
+ Collections.>singleton(RuntimeException.class));
+ repository.setDeleteOnIgnoredException(false);
+ assertThat(repository.getSession(UUID.randomUUID().toString())).isNull();
+ verify(this.delegate, times(1)).getSession(anyString());
+ verifyZeroInteractions(this.delegate);
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void getSessionThrowsNotIgnoredException() {
+ this.thrown.expect(RuntimeException.class);
+ given(this.delegate.getSession(anyString())).willThrow(RuntimeException.class);
+ SafeRetrievingSessionRepository repository =
+ new SafeRetrievingSessionRepository(this.delegate,
+ Collections.>singleton(IllegalStateException.class));
+ repository.getSession(UUID.randomUUID().toString());
+ verify(this.delegate, times(1)).getSession(anyString());
+ verifyZeroInteractions(this.delegate);
+ }
+
+ @Test
+ public void deleteSession() {
+ SafeRetrievingSessionRepository repository =
+ new SafeRetrievingSessionRepository(this.delegate,
+ Collections.>singleton(RuntimeException.class));
+ repository.delete(UUID.randomUUID().toString());
+ verify(this.delegate, times(1)).delete(anyString());
+ verifyZeroInteractions(this.delegate);
+ }
+
+ @Test
+ public void findByIndexNameAndIndexValue() {
+ ExpiringSession session = mock(ExpiringSession.class);
+ given(this.delegate.findByIndexNameAndIndexValue(anyString(), anyString()))
+ .willReturn(Collections.singletonMap("name", session));
+ SafeRetrievingSessionRepository repository =
+ new SafeRetrievingSessionRepository(this.delegate,
+ Collections.>singleton(RuntimeException.class));
+ assertThat(repository.findByIndexNameAndIndexValue("name", "value").get("name"))
+ .isEqualTo(session);
+ verify(this.delegate, times(1)).findByIndexNameAndIndexValue(anyString(),
+ anyString());
+ verifyZeroInteractions(this.delegate);
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void findByIndexNameAndIndexValueThrowsIgnoredException() {
+ given(this.delegate.findByIndexNameAndIndexValue(anyString(), anyString()))
+ .willThrow(RuntimeException.class);
+ SafeRetrievingSessionRepository repository =
+ new SafeRetrievingSessionRepository(this.delegate,
+ Collections.>singleton(RuntimeException.class));
+ assertThat(repository.findByIndexNameAndIndexValue("name", "value")).isEmpty();
+ verify(this.delegate, times(1)).findByIndexNameAndIndexValue(anyString(),
+ anyString());
+ verifyZeroInteractions(this.delegate);
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void findByIndexNameAndIndexValueThrowsNotIgnoredException() {
+ this.thrown.expect(RuntimeException.class);
+ given(this.delegate.findByIndexNameAndIndexValue(anyString(), anyString()))
+ .willThrow(RuntimeException.class);
+ SafeRetrievingSessionRepository repository =
+ new SafeRetrievingSessionRepository(this.delegate,
+ Collections.>singleton(IllegalStateException.class));
+ repository.findByIndexNameAndIndexValue("name", "value");
+ verify(this.delegate, times(1)).findByIndexNameAndIndexValue(anyString(),
+ anyString());
+ verifyZeroInteractions(this.delegate);
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void findByIndexNameAndIndexValueOnSessionRepositoryThrowsException() {
+ this.thrown.expect(UnsupportedOperationException.class);
+ SessionRepository delegate = mock(SessionRepository.class);
+ SafeRetrievingSessionRepository repository =
+ new SafeRetrievingSessionRepository(delegate,
+ Collections.>singleton(IllegalStateException.class));
+ repository.findByIndexNameAndIndexValue("name", "value");
+ verifyZeroInteractions(delegate);
+ }
+
+}