Compare commits

...

45 Commits

Author SHA1 Message Date
Vedran Pavic
bd0f474b5b Release 2.2.0.RC1 2019-10-01 06:25:43 +02:00
Vedran Pavic
e5a3933cb6 Upgrade spring-build-conventions to 0.0.27.RELEASE 2019-10-01 06:09:41 +02:00
Vedran Pavic
71e5cc857a Use Jackson 2.10.0 in samples
See: #1508
2019-10-01 00:28:34 +02:00
Vedran Pavic
df455ddc89 Upgrade Spring Security to 5.2.0.RELEASE
Resolves: #1508
2019-09-30 23:55:50 +02:00
Vedran Pavic
eceeaa665d Use Reactor Dysprosium-RELEASE in samples
See: #1509
2019-09-30 21:47:08 +02:00
Vedran Pavic
e6c54d8a75 Upgrade Spring Data to Moore-RELEASE
Resolves: #1507
2019-09-30 21:42:47 +02:00
Vedran Pavic
c88456a183 Rework scheduling configurers into nested configuration
This commit extracts scheduling configurers that are used in Redis and JDBC configurations into nested configuration classes in order to avoid bean method references.

Resolves: #1516
2019-09-30 16:08:55 +02:00
Vedran Pavic
f5abd55394 Ensure proxyBeanMethods is set to false everywhere
This commit sets proxyBeanMethods to false on all @Enable*Session annotations since they are meta-annotated with @Configuration.

See: #1516
2019-09-30 16:08:55 +02:00
Vedran Pavic
b9fd3666b5 Update integration tests 2019-09-30 16:08:20 +02:00
Vedran Pavic
e06ea36ad5 Upgrade test dependencies 2019-09-30 16:08:18 +02:00
Vedran Pavic
0a1701233e Upgrade Spring Framework to 5.2.0.RELEASE
Resolves: #1506
2019-09-30 09:55:05 +02:00
Vedran Pavic
47a4873199 Align TransactionOperations usage with Spring Framework 5.2.0.RELEASE
See: #1506
2019-09-29 22:12:30 +02:00
Vedran Pavic
bd36e115a8 Align with spring-javaformat 0.0.15 2019-09-29 16:16:36 +02:00
Vedran Pavic
ec82336477 Parallelize JDBC integration tests
See: #1505
2019-09-27 07:14:23 +02:00
Vedran Pavic
feaf8780a8 Add support for configuring custom IndexResolver
See: #1467
2019-09-26 22:18:37 +02:00
Vedran Pavic
b357a76ce3 Align Spring Data Redis dependency excludes with Moore-RELEASE
See: #1507
2019-09-26 14:42:22 +02:00
Vedran Pavic
2c6f22afb0 Upgrade Reactor to Dysprosium-RELEASE
Resolves: #1509
2019-09-25 00:30:42 +02:00
Vedran Pavic
34306fd3a0 Fix Gradle dependency caching configuration
See: #1505
2019-09-24 20:50:58 +02:00
Vedran Pavic
a6c1d8eb1d Tweak Gradle JVM memory settings
See: #1505
2019-09-23 22:35:34 +02:00
Vedran Pavic
e48b46a2d5 Improve support for Oracle integration tests
Resolves: #1510
2019-09-23 22:35:33 +02:00
Vedran Pavic
8cc8fbb7fd Harmonize naming of session repositories
Resolves: #1455
2019-09-22 21:47:53 +02:00
Vedran Pavic
96715e04f2 Start building against Reactor Dysprosium-RELEASE snapshots
See: #1509
2019-09-17 22:12:04 +02:00
Vedran Pavic
121a633a40 Optimize project build
This commit optimizes build by:
- configuring Gradle JVM memory settings
- configuring parallel execution
- disabling caching of snapshots

Resolves: #1505
2019-09-17 22:07:44 +02:00
Vedran Pavic
bf31a9b04b Start building against Spring Security 5.2.0.RELEASE snapshots
See: #1508
2019-09-12 22:47:01 +02:00
Vedran Pavic
a209d436d1 Start building against Spring Data Moore-RELEASE snapshots
See: #1507
2019-09-12 22:46:18 +02:00
Vedran Pavic
6c76a1ccdd Start building against Spring Framework 5.2.0.RELEASE snapshots
See: #1506
2019-09-12 22:45:33 +02:00
Vedran Pavic
c974eeb188 Upgrade samples to Spring Boot 2.2.0.M6
Resolves: #1504
2019-09-11 22:38:38 +02:00
Rob Winch
3b5dadb07f Next Development Version 2019-09-06 11:54:55 -05:00
Rob Winch
3e6b3fda0f Release 2.2.0.M4 2019-09-06 11:36:11 -05:00
Rob Winch
840da7fb5a Update to Spring Security 5.2.0.RC1
Fixes gh-1487
2019-09-06 09:31:24 -05:00
Vedran Pavic
560ee5ff4f Upgrade Spring Data to Moore-RC3
Resolves: #1486
2019-09-06 13:17:30 +02:00
Vedran Pavic
072348e28f Upgrade Gradle to 5.6.2 2019-09-05 22:19:39 +02:00
Vedran Pavic
99dfdda7b7 Upgrade Spring Framework to 5.2.0.RC2
Resolves: #1485
2019-09-05 13:12:34 +02:00
Vedran Pavic
18b097d9c7 Upgrade Reactor to Dysprosium-RC1
Resolves: #1498
2019-09-04 07:12:21 +02:00
Vedran Pavic
702a35fac6 Update integration tests 2019-09-03 22:54:57 +02:00
Vedran Pavic
df3e4c5bc1 Add support for customizing session repository before initialization
This commit adds support for customizing session repository implementations (both SessionRepository and ReactiveSessionRepository) before initialization by introducing SessionRepositoryCustomizer and ReactiveSessionRepositoryCustomizer strategies.

Resolves: #1499
2019-09-03 22:17:36 +02:00
Lars Grefer
f746233255 Upgrade Gradle to 5.6.1
Resolves: #1496
2019-08-30 22:44:42 +02:00
Vedran Pavic
f6c82f1eee Improve support for customizing JDBC session store transaction behavior
Resolves: #1469
2019-08-23 23:26:11 +02:00
Josh Cummings
bcdd05a0bc Add OnCommittedResponseWrapper.setContentLengthLong
Add setContentLengthLong tracking to OnCommittedResponseWrapper in
order to detect commits on servlets that use setContentLengthLong to
announce the entity size they are about to write (as used in the
Apache Tomcat's DefaultServlet).

Fixes gh-1489
2019-08-20 13:29:52 -06:00
Vedran Pavic
5d26ab4df4 Add support for AuthenticatedPrincipal in SpringSessionBackedSessionRegistry
Resolves: #1488
2019-08-10 11:23:25 +02:00
Vedran Pavic
e55d86f5e2 Start building against Spring Security 5.2.0.RC1 snapshots
See: #1487
2019-08-07 21:21:34 +02:00
Vedran Pavic
fe480b338c Start building against Spring Data Moore-RC3 snapshots
See: #1486
2019-08-07 21:13:03 +02:00
Vedran Pavic
4b13392430 Start building against Spring Framework 5.2.0.RC2 snapshots
See: #1485
2019-08-07 21:11:34 +02:00
Vedran Pavic
e5d9ce6ead Upgrade samples to Spring Boot 2.2.0.M5
Resolves: #1484
2019-08-06 18:07:18 +02:00
Vedran Pavic
bc1ef4359a Next development version 2019-08-05 22:18:02 +02:00
115 changed files with 3820 additions and 3258 deletions

View File

@@ -17,4 +17,4 @@ cache:
install: true
script: ./gradlew clean build --refresh-dependencies --no-daemon
script: ./gradlew clean check --no-daemon --stacktrace

14
Jenkinsfile vendored
View File

@@ -18,7 +18,7 @@ try {
sh "git clean -dfx"
try {
withEnv(["JAVA_HOME=${tool 'jdk8'}"]) {
sh './gradlew clean check --no-daemon --refresh-dependencies --stacktrace'
sh './gradlew clean check --no-daemon --stacktrace'
}
}
catch (e) {
@@ -40,7 +40,7 @@ try {
sh "git clean -dfx"
try {
withEnv(["JAVA_HOME=${tool 'jdk9'}"]) {
sh './gradlew clean test --no-daemon --refresh-dependencies --stacktrace'
sh './gradlew clean test --no-daemon --stacktrace'
}
}
catch (e) {
@@ -59,7 +59,7 @@ try {
sh "git clean -dfx"
try {
withEnv(["JAVA_HOME=${tool 'jdk10'}"]) {
sh './gradlew clean test --no-daemon --refresh-dependencies --stacktrace'
sh './gradlew clean test --no-daemon --stacktrace'
}
}
catch (e) {
@@ -78,7 +78,7 @@ try {
sh "git clean -dfx"
try {
withEnv(["JAVA_HOME=${tool 'jdk11'}"]) {
sh './gradlew clean test integrationTest --no-daemon --refresh-dependencies --stacktrace'
sh './gradlew clean test integrationTest --no-daemon --stacktrace'
}
}
catch (e) {
@@ -96,7 +96,7 @@ try {
checkout scm
try {
withEnv(["JAVA_HOME=${tool 'openjdk12'}"]) {
sh './gradlew clean test integrationTest --no-daemon --refresh-dependencies --stacktrace'
sh './gradlew clean test integrationTest --no-daemon --stacktrace'
}
}
catch (e) {
@@ -120,7 +120,7 @@ try {
withCredentials([usernamePassword(credentialsId: 'oss-token', passwordVariable: 'OSSRH_PASSWORD', usernameVariable: 'OSSRH_USERNAME')]) {
withCredentials([usernamePassword(credentialsId: '02bd1690-b54f-4c9f-819d-a77cb7a9822c', usernameVariable: 'ARTIFACTORY_USERNAME', passwordVariable: 'ARTIFACTORY_PASSWORD')]) {
withEnv(["JAVA_HOME=${tool 'jdk8'}"]) {
sh './gradlew deployArtifacts finalizeDeployArtifacts --no-daemon --refresh-dependencies --stacktrace -Psigning.secretKeyRingFile=$SIGNING_KEYRING_FILE -Psigning.keyId=$SPRING_SIGNING_KEYID -Psigning.password=$SIGNING_PASSWORD -PossrhUsername=$OSSRH_USERNAME -PossrhPassword=$OSSRH_PASSWORD -PartifactoryUsername=$ARTIFACTORY_USERNAME -PartifactoryPassword=$ARTIFACTORY_PASSWORD'
sh './gradlew deployArtifacts finalizeDeployArtifacts --no-daemon --stacktrace -Psigning.secretKeyRingFile=$SIGNING_KEYRING_FILE -Psigning.keyId=$SPRING_SIGNING_KEYID -Psigning.password=$SIGNING_PASSWORD -PossrhUsername=$OSSRH_USERNAME -PossrhPassword=$OSSRH_PASSWORD -PartifactoryUsername=$ARTIFACTORY_USERNAME -PartifactoryPassword=$ARTIFACTORY_PASSWORD'
}
}
}
@@ -142,7 +142,7 @@ try {
try {
withCredentials([file(credentialsId: 'docs.spring.io-jenkins_private_ssh_key', variable: 'DEPLOY_SSH_KEY')]) {
withEnv(["JAVA_HOME=${tool 'jdk8'}"]) {
sh './gradlew deployDocs --no-daemon --refresh-dependencies --stacktrace -PdeployDocsSshKeyPath=$DEPLOY_SSH_KEY -PdeployDocsSshUsername=$SPRING_DOCS_USERNAME'
sh './gradlew deployDocs --no-daemon --stacktrace -PdeployDocsSshKeyPath=$DEPLOY_SSH_KEY -PdeployDocsSshUsername=$SPRING_DOCS_USERNAME'
}
}
}

View File

@@ -4,7 +4,7 @@ buildscript {
snapshotBuild = version.endsWith('SNAPSHOT')
milestoneBuild = !(releaseBuild || snapshotBuild)
springBootVersion = '2.2.0.M4'
springBootVersion = '2.2.0.M6'
}
repositories {
@@ -13,7 +13,7 @@ buildscript {
}
dependencies {
classpath 'io.spring.gradle:spring-build-conventions:0.0.26.RELEASE'
classpath 'io.spring.gradle:spring-build-conventions:0.0.27.RELEASE'
classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion"
}
}
@@ -28,9 +28,9 @@ subprojects {
plugins.withType(JavaPlugin) {
sourceCompatibility = JavaVersion.VERSION_1_8
}
tasks.withType(Test) {
useJUnitPlatform()
}
tasks.withType(Test) {
useJUnitPlatform()
}
}

View File

@@ -7,5 +7,5 @@
<suppress files="[\\/]spring-session-docs[\\/]" checks="InnerTypeLast"/>
<suppress files="[\\/]spring-session-samples[\\/]" checks="Javadoc*"/>
<suppress files="[\\/]spring-session-samples[\\/].+Application\.java" checks="HideUtilityClassConstructor"/>
<suppress files="SessionRepositoryFilterTests\.java" checks="SpringLambda"/>
<suppress files="CookieSerializer\.java" checks="SpringMethodVisibility"/>
</suppressions>

View File

@@ -1 +1,3 @@
version=2.2.0.M3
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.parallel=true
version=2.2.0.RC1

View File

@@ -1,11 +1,10 @@
dependencyManagement {
imports {
mavenBom 'com.fasterxml.jackson:jackson-bom:2.9.6'
mavenBom 'io.projectreactor:reactor-bom:Dysprosium-M3'
mavenBom 'org.junit:junit-bom:5.5.1'
mavenBom 'org.springframework:spring-framework-bom:5.2.0.RC1'
mavenBom 'org.springframework.data:spring-data-releasetrain:Moore-RC2'
mavenBom 'org.springframework.security:spring-security-bom:5.2.0.M4'
mavenBom 'io.projectreactor:reactor-bom:Dysprosium-RELEASE'
mavenBom 'org.junit:junit-bom:5.5.2'
mavenBom 'org.springframework:spring-framework-bom:5.2.0.RELEASE'
mavenBom 'org.springframework.data:spring-data-releasetrain:Moore-RELEASE'
mavenBom 'org.springframework.security:spring-security-bom:5.2.0.RELEASE'
mavenBom 'org.testcontainers:testcontainers-bom:1.12.0'
}
@@ -18,17 +17,19 @@ dependencyManagement {
dependency 'com.h2database:h2:1.4.199'
dependency 'com.ibm.db2:jcc:11.5.0.0'
dependency 'com.microsoft.sqlserver:mssql-jdbc:7.4.1.jre8'
dependency 'com.zaxxer:HikariCP:3.3.1'
dependency 'com.oracle.ojdbc:ojdbc8:19.3.0.0'
dependency 'com.zaxxer:HikariCP:3.4.1'
dependency 'edu.umd.cs.mtc:multithreadedtc:1.01'
dependency 'io.lettuce:lettuce-core:5.1.8.RELEASE'
dependency 'io.lettuce:lettuce-core:5.2.0.RELEASE'
dependency 'javax.annotation:javax.annotation-api:1.3.2'
dependency 'javax.servlet:javax.servlet-api:4.0.1'
dependency 'junit:junit:4.12'
dependency 'mysql:mysql-connector-java:8.0.17'
dependency 'org.apache.derby:derby:10.14.2.0'
dependency 'org.assertj:assertj-core:3.13.2'
dependency 'org.hsqldb:hsqldb:2.5.0'
dependency 'org.mariadb.jdbc:mariadb-java-client:2.4.3'
dependency 'org.mariadb.jdbc:mariadb-java-client:2.4.4'
dependency 'org.mockito:mockito-core:3.0.0'
dependency 'org.postgresql:postgresql:42.2.6'
dependency 'org.postgresql:postgresql:42.2.8'
}
}

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.5.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

4
gradlew vendored
View File

@@ -125,8 +125,8 @@ if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2014-2019 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
*
* https://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.config;
import org.springframework.session.ReactiveSessionRepository;
/**
* Strategy that can be used to customize the {@link ReactiveSessionRepository} before it
* is fully initialized, in particular to tune its configuration.
*
* @param <T> the {@link ReactiveSessionRepository} type
* @author Vedran Pavic
* @since 2.2.0
*/
@FunctionalInterface
public interface ReactiveSessionRepositoryCustomizer<T extends ReactiveSessionRepository> {
/**
* Customize the {@link ReactiveSessionRepository}.
* @param sessionRepository the {@link ReactiveSessionRepository} to customize
*/
void customize(T sessionRepository);
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2014-2019 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
*
* https://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.config;
import org.springframework.session.SessionRepository;
/**
* Strategy that can be used to customize the {@link SessionRepository} before it is fully
* initialized, in particular to tune its configuration.
*
* @param <T> the {@link SessionRepository} type
* @author Vedran Pavic
* @since 2.2.0
*/
@FunctionalInterface
public interface SessionRepositoryCustomizer<T extends SessionRepository> {
/**
* Customize the {@link SessionRepository}.
* @param sessionRepository the {@link SessionRepository} to customize
*/
void customize(T sessionRepository);
}

View File

@@ -74,7 +74,7 @@ import org.springframework.session.events.SessionDestroyedEvent;
@Target({ java.lang.annotation.ElementType.TYPE })
@Documented
@Import(SpringHttpSessionConfiguration.class)
@Configuration
@Configuration(proxyBeanMethods = false)
public @interface EnableSpringHttpSession {
}

View File

@@ -49,7 +49,7 @@ import org.springframework.context.annotation.Import;
@Target({ java.lang.annotation.ElementType.TYPE })
@Documented
@Import(SpringWebSessionConfiguration.class)
@Configuration
@Configuration(proxyBeanMethods = false)
public @interface EnableSpringWebSession {
}

View File

@@ -16,14 +16,13 @@
package org.springframework.session.security;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.util.Assert;
@@ -110,13 +109,8 @@ public class SpringSessionBackedSessionRegistry<S extends Session> implements Se
* could be derived
*/
protected String name(Object principal) {
if (principal instanceof UserDetails) {
return ((UserDetails) principal).getUsername();
}
if (principal instanceof Principal) {
return ((Principal) principal).getName();
}
return principal.toString();
// We are reusing the logic from AbstractAuthenticationToken#getName
return new TestingAuthenticationToken(principal, null).getName();
}
}

View File

@@ -65,11 +65,7 @@ class HttpSessionAdapter<S extends Session> implements HttpSession {
this.servletContext = servletContext;
}
public void setSession(S session) {
this.session = session;
}
public S getSession() {
S getSession() {
return this.session;
}
@@ -191,16 +187,16 @@ class HttpSessionAdapter<S extends Session> implements HttpSession {
this.invalidated = true;
}
public void setNew(boolean isNew) {
this.old = !isNew;
}
@Override
public boolean isNew() {
checkState();
return !this.old;
}
void markNotNew() {
this.old = true;
}
private void checkState() {
if (this.invalidated) {
throw new IllegalStateException("The HttpSession has already be invalidated.");

View File

@@ -25,9 +25,6 @@ import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Base class for response wrappers which encapsulate the logic for handling an event when
* the {@link javax.servlet.http.HttpServletResponse} is committed.
@@ -37,8 +34,6 @@ import org.apache.commons.logging.LogFactory;
*/
abstract class OnCommittedResponseWrapper extends HttpServletResponseWrapper {
private final Log logger = LogFactory.getLog(getClass());
private boolean disableOnCommitted;
/**
@@ -69,6 +64,12 @@ abstract class OnCommittedResponseWrapper extends HttpServletResponseWrapper {
super.addHeader(name, value);
}
@Override
public void setContentLengthLong(long len) {
setContentLength(len);
super.setContentLengthLong(len);
}
@Override
public void setContentLength(int len) {
setContentLength((long) len);
@@ -86,7 +87,7 @@ abstract class OnCommittedResponseWrapper extends HttpServletResponseWrapper {
* {@link javax.servlet.http.HttpServletResponse} is committed. This can be useful in
* the event that Async Web Requests are made.
*/
public void disableOnResponseCommitted() {
private void disableOnResponseCommitted() {
this.disableOnCommitted = true;
}

View File

@@ -292,7 +292,7 @@ public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFi
requestedSession.setLastAccessedTime(Instant.now());
this.requestedSessionIdValid = true;
currentSession = new HttpSessionWrapper(requestedSession, getServletContext());
currentSession.setNew(false);
currentSession.markNotNew();
setCurrentSession(currentSession);
return currentSession;
}

View File

@@ -57,6 +57,7 @@ class DelegatingIndexResolverTests {
this.supportedIndex = supportedIndex;
}
@Override
public Map<String, String> resolveIndexesFor(MapSession session) {
return Collections.singletonMap(this.supportedIndex, session.getAttribute(this.supportedIndex));
}

View File

@@ -126,12 +126,12 @@ class EnableSpringHttpSessionCustomCookieSerializerTests {
static class Config {
@Bean
public SessionRepository sessionRepository() {
SessionRepository sessionRepository() {
return mock(SessionRepository.class);
}
@Bean
public CookieSerializer cookieSerializer() {
CookieSerializer cookieSerializer() {
return mock(CookieSerializer.class);
}

View File

@@ -120,7 +120,7 @@ class SpringHttpSessionConfigurationTests {
static class BaseConfiguration {
@Bean
public MapSessionRepository sessionRepository() {
MapSessionRepository sessionRepository() {
return new MapSessionRepository(new ConcurrentHashMap<>());
}
@@ -137,7 +137,7 @@ class SpringHttpSessionConfigurationTests {
static class SessionCookieConfigConfiguration extends BaseConfiguration {
@Bean
public ServletContext servletContext() {
ServletContext servletContext() {
MockServletContext servletContext = new MockServletContext();
servletContext.getSessionCookieConfig().setName("test-name");
servletContext.getSessionCookieConfig().setDomain("test-domain");
@@ -153,7 +153,7 @@ class SpringHttpSessionConfigurationTests {
static class RememberMeServicesConfiguration extends BaseConfiguration {
@Bean
public SpringSessionRememberMeServices rememberMeServices() {
SpringSessionRememberMeServices rememberMeServices() {
return new SpringSessionRememberMeServices();
}

View File

@@ -16,6 +16,7 @@
package org.springframework.session.security;
import java.security.Principal;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
@@ -30,6 +31,7 @@ import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.security.core.AuthenticatedPrincipal;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.core.session.SessionInformation;
@@ -104,11 +106,25 @@ class SpringSessionBackedSessionRegistryTest {
}
@Test
void getAllSessions() {
void getAllSessionsForUserDetails() {
setUpSessions();
List<SessionInformation> allSessionInfos = this.sessionRegistry.getAllSessions(PRINCIPAL, true);
assertThat(allSessionInfos).extracting("sessionId").containsExactly(SESSION_ID, SESSION_ID2);
}
@Test
void getAllSessionsForAuthenticatedPrincipal() {
setUpSessions();
List<SessionInformation> allSessionInfos = this.sessionRegistry
.getAllSessions((AuthenticatedPrincipal) () -> USER_NAME, true);
assertThat(allSessionInfos).extracting("sessionId").containsExactly(SESSION_ID, SESSION_ID2);
}
@Test
void getAllSessionsForPrincipal() {
setUpSessions();
List<SessionInformation> allSessionInfos = this.sessionRegistry.getAllSessions(new TestPrincipal(USER_NAME),
true);
assertThat(allSessionInfos).extracting("sessionId").containsExactly(SESSION_ID, SESSION_ID2);
}
@@ -159,4 +175,40 @@ class SpringSessionBackedSessionRegistryTest {
when(this.sessionRepository.findByPrincipalName(USER_NAME)).thenReturn(sessions);
}
private static final class TestPrincipal implements Principal {
private final String name;
private TestPrincipal(String name) {
this.name = name;
}
@Override
public String getName() {
return this.name;
}
@Override
public boolean equals(Object another) {
if (this == another) {
return true;
}
if (another instanceof TestPrincipal) {
return this.name.equals(((TestPrincipal) another).name);
}
return false;
}
@Override
public int hashCode() {
return this.name.hashCode();
}
@Override
public String toString() {
return this.name;
}
}
}

View File

@@ -499,7 +499,7 @@ class DefaultCookieSerializerTests {
assertThat(getCookie().getSameSite()).isNull();
}
public void setCookieName(String cookieName) {
void setCookieName(String cookieName) {
this.cookieName = cookieName;
this.serializer.setCookieName(cookieName);
}

View File

@@ -1100,6 +1100,17 @@ class OnCommittedResponseWrapperTests {
assertThat(this.committed).isTrue();
}
// gh-7261
@Test
void contentLengthLongOutputStreamWriteStringCommits() throws IOException {
String body = "something";
this.response.setContentLengthLong(body.length());
this.response.getOutputStream().print(body);
assertThat(this.committed).isTrue();
}
@Test
void bufferSizeCommitsOnce() throws Exception {
String expected = "1234567890";

View File

@@ -1331,11 +1331,8 @@ class SessionRepositoryFilterTests {
// We want the filter to work without any dependencies on Spring
@Test
@SuppressWarnings("unused")
void doesNotImplementOrdered() {
assertThatExceptionOfType(ClassCastException.class).isThrownBy(() -> {
Ordered o = (Ordered) this.filter;
});
assertThatExceptionOfType(ClassCastException.class).isThrownBy(() -> Ordered.class.cast(this.filter));
}
@Test

View File

@@ -5,7 +5,6 @@ description = "Spring Session Redis implementation"
dependencies {
compile project(':spring-session-core')
compile ("org.springframework.data:spring-data-redis") {
exclude group: "org.slf4j", module: 'slf4j-api'
exclude group: "org.slf4j", module: 'jcl-over-slf4j'
}

View File

@@ -23,13 +23,13 @@ import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
/**
* Base class for {@link RedisOperationsSessionRepository} integration tests.
* Base class for Redis integration tests.
*
* @author Vedran Pavic
*/
public abstract class AbstractRedisITests {
private static final String DOCKER_IMAGE = "redis:5.0.5";
private static final String DOCKER_IMAGE = "redis:5.0.6";
protected static class BaseConfig {

View File

@@ -25,6 +25,7 @@ import reactor.core.publisher.Mono;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.Session;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository.RedisSession;
import org.springframework.session.data.redis.config.annotation.web.server.EnableRedisWebSession;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@@ -34,21 +35,21 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Integration tests for {@link ReactiveRedisOperationsSessionRepository}.
* Integration tests for {@link ReactiveRedisSessionRepository}.
*
* @author Vedran Pavic
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
class ReactiveRedisOperationsSessionRepositoryITests extends AbstractRedisITests {
class ReactiveRedisSessionRepositoryITests extends AbstractRedisITests {
@Autowired
private ReactiveRedisOperationsSessionRepository repository;
private ReactiveRedisSessionRepository repository;
@Test
void saves() {
ReactiveRedisOperationsSessionRepository.RedisSession toSave = this.repository.createSession().block();
RedisSession toSave = this.repository.createSession().block();
String expectedAttributeName = "a";
String expectedAttributeValue = "b";
@@ -70,7 +71,7 @@ class ReactiveRedisOperationsSessionRepositoryITests extends AbstractRedisITests
@Test // gh-1399
void saveMultipleTimes() {
ReactiveRedisOperationsSessionRepository.RedisSession session = this.repository.createSession().block();
RedisSession session = this.repository.createSession().block();
session.setAttribute("attribute1", "value1");
Mono<Void> save1 = this.repository.save(session);
session.setAttribute("attribute2", "value2");
@@ -80,7 +81,7 @@ class ReactiveRedisOperationsSessionRepositoryITests extends AbstractRedisITests
@Test
void putAllOnSingleAttrDoesNotRemoveOld() {
ReactiveRedisOperationsSessionRepository.RedisSession toSave = this.repository.createSession().block();
RedisSession toSave = this.repository.createSession().block();
toSave.setAttribute("a", "b");
this.repository.save(toSave).block();
@@ -103,13 +104,12 @@ class ReactiveRedisOperationsSessionRepositoryITests extends AbstractRedisITests
void changeSessionIdWhenOnlyChangeId() {
String attrName = "changeSessionId";
String attrValue = "changeSessionId-value";
ReactiveRedisOperationsSessionRepository.RedisSession toSave = this.repository.createSession().block();
RedisSession toSave = this.repository.createSession().block();
toSave.setAttribute(attrName, attrValue);
this.repository.save(toSave).block();
ReactiveRedisOperationsSessionRepository.RedisSession findById = this.repository.findById(toSave.getId())
.block();
RedisSession findById = this.repository.findById(toSave.getId()).block();
assertThat(findById.<String>getAttribute(attrName)).isEqualTo(attrValue);
@@ -120,15 +120,14 @@ class ReactiveRedisOperationsSessionRepositoryITests extends AbstractRedisITests
assertThat(this.repository.findById(originalFindById).block()).isNull();
ReactiveRedisOperationsSessionRepository.RedisSession findByChangeSessionId = this.repository
.findById(changeSessionId).block();
RedisSession findByChangeSessionId = this.repository.findById(changeSessionId).block();
assertThat(findByChangeSessionId.<String>getAttribute(attrName)).isEqualTo(attrValue);
}
@Test
void changeSessionIdWhenChangeTwice() {
ReactiveRedisOperationsSessionRepository.RedisSession toSave = this.repository.createSession().block();
RedisSession toSave = this.repository.createSession().block();
this.repository.save(toSave).block();
@@ -148,12 +147,11 @@ class ReactiveRedisOperationsSessionRepositoryITests extends AbstractRedisITests
String attrName = "changeSessionId";
String attrValue = "changeSessionId-value";
ReactiveRedisOperationsSessionRepository.RedisSession toSave = this.repository.createSession().block();
RedisSession toSave = this.repository.createSession().block();
this.repository.save(toSave).block();
ReactiveRedisOperationsSessionRepository.RedisSession findById = this.repository.findById(toSave.getId())
.block();
RedisSession findById = this.repository.findById(toSave.getId()).block();
findById.setAttribute(attrName, attrValue);
@@ -164,15 +162,14 @@ class ReactiveRedisOperationsSessionRepositoryITests extends AbstractRedisITests
assertThat(this.repository.findById(originalFindById).block()).isNull();
ReactiveRedisOperationsSessionRepository.RedisSession findByChangeSessionId = this.repository
.findById(changeSessionId).block();
RedisSession findByChangeSessionId = this.repository.findById(changeSessionId).block();
assertThat(findByChangeSessionId.<String>getAttribute(attrName)).isEqualTo(attrValue);
}
@Test
void changeSessionIdWhenHasNotSaved() {
ReactiveRedisOperationsSessionRepository.RedisSession toSave = this.repository.createSession().block();
RedisSession toSave = this.repository.createSession().block();
String originalId = toSave.getId();
toSave.changeSessionId();
@@ -185,7 +182,7 @@ class ReactiveRedisOperationsSessionRepositoryITests extends AbstractRedisITests
// gh-954
@Test
void changeSessionIdSaveTwice() {
ReactiveRedisOperationsSessionRepository.RedisSession toSave = this.repository.createSession().block();
RedisSession toSave = this.repository.createSession().block();
String originalId = toSave.getId();
toSave.changeSessionId();
@@ -199,12 +196,12 @@ class ReactiveRedisOperationsSessionRepositoryITests extends AbstractRedisITests
// gh-1111
@Test
void changeSessionSaveOldSessionInstance() {
ReactiveRedisOperationsSessionRepository.RedisSession toSave = this.repository.createSession().block();
RedisSession toSave = this.repository.createSession().block();
String sessionId = toSave.getId();
this.repository.save(toSave).block();
ReactiveRedisOperationsSessionRepository.RedisSession session = this.repository.findById(sessionId).block();
RedisSession session = this.repository.findById(sessionId).block();
session.changeSessionId();
session.setLastAccessedTime(Instant.now());
this.repository.save(session).block();

View File

@@ -37,7 +37,7 @@ import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.data.SessionEventRegistry;
import org.springframework.session.data.redis.RedisOperationsSessionRepository.RedisSession;
import org.springframework.session.data.redis.RedisIndexedSessionRepository.RedisSession;
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisOperations;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.session.events.SessionCreatedEvent;
@@ -48,17 +48,23 @@ import org.springframework.test.context.web.WebAppConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link RedisIndexedSessionRepository}.
*
* @author Rob Winch
* @author Vedran Pavic
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
class RedisOperationsSessionRepositoryITests extends AbstractRedisITests {
class RedisIndexedSessionRepositoryITests extends AbstractRedisITests {
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
private static final String INDEX_NAME = FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME;
@Autowired
private RedisOperationsSessionRepository repository;
private RedisIndexedSessionRepository repository;
@Autowired
private SessionEventRegistry registry;
@@ -88,7 +94,7 @@ class RedisOperationsSessionRepositoryITests extends AbstractRedisITests {
void saves() throws InterruptedException {
String username = "saves-" + System.currentTimeMillis();
String usernameSessionKey = "RedisOperationsSessionRepositoryITests:index:" + INDEX_NAME + ":" + username;
String usernameSessionKey = "RedisIndexedSessionRepositoryITests:index:" + INDEX_NAME + ":" + username;
RedisSession toSave = this.repository.createSession();
String expectedAttributeName = "a";
@@ -180,7 +186,7 @@ class RedisOperationsSessionRepositoryITests extends AbstractRedisITests {
this.repository.save(toSave);
String body = "RedisOperationsSessionRepositoryITests:sessions:expires:" + toSave.getId();
String body = "RedisIndexedSessionRepositoryITests:sessions:expires:" + toSave.getId();
String channel = "__keyevent@0__:expired";
DefaultMessage message = new DefaultMessage(channel.getBytes(StandardCharsets.UTF_8),
body.getBytes(StandardCharsets.UTF_8));
@@ -342,7 +348,7 @@ class RedisOperationsSessionRepositoryITests extends AbstractRedisITests {
this.repository.save(toSave);
String body = "RedisOperationsSessionRepositoryITests:sessions:expires:" + toSave.getId();
String body = "RedisIndexedSessionRepositoryITests:sessions:expires:" + toSave.getId();
String channel = "__keyevent@0__:expired";
DefaultMessage message = new DefaultMessage(channel.getBytes(StandardCharsets.UTF_8),
body.getBytes(StandardCharsets.UTF_8));
@@ -607,11 +613,11 @@ class RedisOperationsSessionRepositoryITests extends AbstractRedisITests {
}
@Configuration
@EnableRedisHttpSession(redisNamespace = "RedisOperationsSessionRepositoryITests")
@EnableRedisHttpSession(redisNamespace = "RedisIndexedSessionRepositoryITests")
static class Config extends BaseConfig {
@Bean
public SessionEventRegistry sessionEventRegistry() {
SessionEventRegistry sessionEventRegistry() {
return new SessionEventRegistry();
}

View File

@@ -32,7 +32,7 @@ import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.session.MapSession;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
import org.springframework.session.data.redis.SimpleRedisOperationsSessionRepository.RedisSession;
import org.springframework.session.data.redis.RedisSessionRepository.RedisSession;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
@@ -41,17 +41,17 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Integration tests for {@link SimpleRedisOperationsSessionRepository}.
* Integration tests for {@link RedisSessionRepository}.
*
* @author Vedran Pavic
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
class SimpleRedisOperationsSessionRepositoryITests extends AbstractRedisITests {
class RedisSessionRepositoryITests extends AbstractRedisITests {
@Autowired
private SimpleRedisOperationsSessionRepository sessionRepository;
private RedisSessionRepository sessionRepository;
@Test
void save_NewSession_ShouldSaveSession() {
@@ -227,11 +227,11 @@ class SimpleRedisOperationsSessionRepositoryITests extends AbstractRedisITests {
static class Config extends BaseConfig {
@Bean
public SimpleRedisOperationsSessionRepository sessionRepository(RedisConnectionFactory redisConnectionFactory) {
RedisSessionRepository sessionRepository(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.afterPropertiesSet();
return new SimpleRedisOperationsSessionRepository(redisTemplate);
return new RedisSessionRepository(redisTemplate);
}
}

View File

@@ -102,11 +102,11 @@ class EnableRedisHttpSessionExpireSessionDestroyedTests<S extends Session> exten
}
}
public boolean receivedEvent() {
boolean receivedEvent() {
return this.receivedEvent;
}
public void setLock(Object lock) {
void setLock(Object lock) {
this.lock = lock;
}
@@ -117,7 +117,7 @@ class EnableRedisHttpSessionExpireSessionDestroyedTests<S extends Session> exten
static class Config extends BaseConfig {
@Bean
public SessionExpiredEventRegistry sessionDestroyedEventRegistry() {
SessionExpiredEventRegistry sessionDestroyedEventRegistry() {
return new SessionExpiredEventRegistry();
}

View File

@@ -35,7 +35,7 @@ import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
class RedisOperationsSessionRepositoryFlushImmediatelyITests<S extends Session> extends AbstractRedisITests {
class RedisIndexedSessionRepositoryFlushImmediatelyITests<S extends Session> extends AbstractRedisITests {
@Autowired
private SessionRepository<S> sessionRepository;

View File

@@ -88,7 +88,7 @@ class RedisListenerContainerTaskExecutorITests extends AbstractRedisITests {
}
}
public boolean taskDispatched() throws InterruptedException {
boolean taskDispatched() throws InterruptedException {
if (this.taskDispatched != null) {
return this.taskDispatched;
}
@@ -105,12 +105,12 @@ class RedisListenerContainerTaskExecutorITests extends AbstractRedisITests {
static class Config extends BaseConfig {
@Bean
public Executor springSessionRedisTaskExecutor() {
Executor springSessionRedisTaskExecutor() {
return new SessionTaskExecutor(Executors.newSingleThreadExecutor());
}
@Bean
public Executor springSessionRedisSubscriptionExecutor() {
Executor springSessionRedisSubscriptionExecutor() {
return new SimpleAsyncTaskExecutor();
}

View File

@@ -16,71 +16,29 @@
package org.springframework.session.data.redis;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import org.springframework.data.redis.core.ReactiveRedisOperations;
import org.springframework.session.MapSession;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.util.Assert;
/**
* A {@link ReactiveSessionRepository} that is implemented using Spring Data's
* {@link ReactiveRedisOperations}.
* This {@link ReactiveSessionRepository} implementation is kept in order to support
* migration to {@link ReactiveRedisSessionRepository} in a backwards compatible manner.
*
* @author Vedran Pavic
* @since 2.0
* @since 2.0.0
* @deprecated since 2.2.0 in favor of {@link ReactiveRedisSessionRepository}
*/
public class ReactiveRedisOperationsSessionRepository
implements ReactiveSessionRepository<ReactiveRedisOperationsSessionRepository.RedisSession> {
@Deprecated
public class ReactiveRedisOperationsSessionRepository extends ReactiveRedisSessionRepository {
/**
* The default namespace for each key and channel in Redis used by Spring Session.
* Create a new {@link ReactiveRedisOperationsSessionRepository} instance.
* @param sessionRedisOperations the {@link ReactiveRedisOperations} to use for
* managing sessions
* @see ReactiveRedisSessionRepository#ReactiveRedisSessionRepository(ReactiveRedisOperations)
*/
public static final String DEFAULT_NAMESPACE = "spring:session";
private final ReactiveRedisOperations<String, Object> sessionRedisOperations;
/**
* The namespace for every key used by Spring Session in Redis.
*/
private String namespace = DEFAULT_NAMESPACE + ":";
/**
* If non-null, this value is used to override the default value for
* {@link RedisSession#setMaxInactiveInterval(Duration)}.
*/
private Integer defaultMaxInactiveInterval;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
public ReactiveRedisOperationsSessionRepository(ReactiveRedisOperations<String, Object> sessionRedisOperations) {
Assert.notNull(sessionRedisOperations, "sessionRedisOperations cannot be null");
this.sessionRedisOperations = sessionRedisOperations;
}
public void setRedisKeyNamespace(String namespace) {
Assert.hasText(namespace, "namespace cannot be null or empty");
this.namespace = namespace.trim() + ":";
}
/**
* Sets 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 number of seconds that the {@link Session}
* should be kept alive between client requests.
*/
public void setDefaultMaxInactiveInterval(int defaultMaxInactiveInterval) {
this.defaultMaxInactiveInterval = defaultMaxInactiveInterval;
super(sessionRedisOperations);
}
/**
@@ -93,226 +51,4 @@ public class ReactiveRedisOperationsSessionRepository
Assert.notNull(redisFlushMode, "redisFlushMode cannot be null");
}
/**
* Set the save mode.
* @param saveMode the save mode
*/
public void setSaveMode(SaveMode saveMode) {
Assert.notNull(saveMode, "saveMode must not be null");
this.saveMode = saveMode;
}
/**
* Returns the {@link ReactiveRedisOperations} used for sessions.
* @return the {@link ReactiveRedisOperations} used for sessions
* @since 2.1.0
*/
public ReactiveRedisOperations<String, Object> getSessionRedisOperations() {
return this.sessionRedisOperations;
}
@Override
public Mono<RedisSession> createSession() {
return Mono.defer(() -> {
MapSession cached = new MapSession();
if (this.defaultMaxInactiveInterval != null) {
cached.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
}
RedisSession session = new RedisSession(cached, true);
return Mono.just(session);
});
}
@Override
public Mono<Void> save(RedisSession session) {
if (session.isNew) {
return session.save();
}
String sessionKey = getSessionKey(session.hasChangedSessionId() ? session.originalSessionId : session.getId());
return this.sessionRedisOperations.hasKey(sessionKey).flatMap(
(exists) -> exists ? session.save() : Mono.error(new IllegalStateException("Session was invalidated")));
}
@Override
public Mono<RedisSession> findById(String id) {
String sessionKey = getSessionKey(id);
// @formatter:off
return this.sessionRedisOperations.opsForHash().entries(sessionKey)
.collectMap((e) -> e.getKey().toString(), Map.Entry::getValue)
.filter((map) -> !map.isEmpty())
.map(new RedisSessionMapper(id))
.filter((session) -> !session.isExpired())
.map((session) -> new RedisSession(session, false))
.switchIfEmpty(Mono.defer(() -> deleteById(id).then(Mono.empty())));
// @formatter:on
}
@Override
public Mono<Void> deleteById(String id) {
String sessionKey = getSessionKey(id);
return this.sessionRedisOperations.delete(sessionKey).then();
}
private static String getAttributeKey(String attributeName) {
return RedisSessionMapper.ATTRIBUTE_PREFIX + attributeName;
}
private String getSessionKey(String sessionId) {
return this.namespace + "sessions:" + sessionId;
}
/**
* A custom implementation of {@link Session} that uses a {@link MapSession} as the
* basis for its mapping. It keeps track of any attributes that have changed. When
* {@link RedisSession#saveDelta()} is invoked all the attributes that have been
* changed will be persisted.
*/
final class RedisSession implements Session {
private final MapSession cached;
private final Map<String, Object> delta = new HashMap<>();
private boolean isNew;
private String originalSessionId;
RedisSession(MapSession cached, boolean isNew) {
this.cached = cached;
this.isNew = isNew;
this.originalSessionId = cached.getId();
if (this.isNew) {
this.delta.put(RedisSessionMapper.CREATION_TIME_KEY, cached.getCreationTime().toEpochMilli());
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY,
(int) cached.getMaxInactiveInterval().getSeconds());
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, cached.getLastAccessedTime().toEpochMilli());
}
if (this.isNew || (ReactiveRedisOperationsSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
getAttributeNames().forEach((attributeName) -> this.delta.put(getAttributeKey(attributeName),
cached.getAttribute(attributeName)));
}
}
@Override
public String getId() {
return this.cached.getId();
}
@Override
public String changeSessionId() {
return this.cached.changeSessionId();
}
@Override
public <T> T getAttribute(String attributeName) {
T attributeValue = this.cached.getAttribute(attributeName);
if (attributeValue != null
&& ReactiveRedisOperationsSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
this.delta.put(getAttributeKey(attributeName), attributeValue);
}
return attributeValue;
}
@Override
public Set<String> getAttributeNames() {
return this.cached.getAttributeNames();
}
@Override
public void setAttribute(String attributeName, Object attributeValue) {
this.cached.setAttribute(attributeName, attributeValue);
this.delta.put(getAttributeKey(attributeName), attributeValue);
}
@Override
public void removeAttribute(String attributeName) {
this.cached.removeAttribute(attributeName);
this.delta.put(getAttributeKey(attributeName), null);
}
@Override
public Instant getCreationTime() {
return this.cached.getCreationTime();
}
@Override
public void setLastAccessedTime(Instant lastAccessedTime) {
this.cached.setLastAccessedTime(lastAccessedTime);
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, getLastAccessedTime().toEpochMilli());
}
@Override
public Instant getLastAccessedTime() {
return this.cached.getLastAccessedTime();
}
@Override
public void setMaxInactiveInterval(Duration interval) {
this.cached.setMaxInactiveInterval(interval);
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, (int) getMaxInactiveInterval().getSeconds());
}
@Override
public Duration getMaxInactiveInterval() {
return this.cached.getMaxInactiveInterval();
}
@Override
public boolean isExpired() {
return this.cached.isExpired();
}
private boolean hasChangedSessionId() {
return !getId().equals(this.originalSessionId);
}
private Mono<Void> save() {
return Mono.defer(() -> saveChangeSessionId().then(saveDelta()).doOnSuccess((aVoid) -> this.isNew = false));
}
private Mono<Void> saveDelta() {
if (this.delta.isEmpty()) {
return Mono.empty();
}
String sessionKey = getSessionKey(getId());
Mono<Boolean> update = ReactiveRedisOperationsSessionRepository.this.sessionRedisOperations.opsForHash()
.putAll(sessionKey, new HashMap<>(this.delta));
Mono<Boolean> setTtl = ReactiveRedisOperationsSessionRepository.this.sessionRedisOperations
.expire(sessionKey, getMaxInactiveInterval());
return update.and(setTtl).and((s) -> {
this.delta.clear();
s.onComplete();
}).then();
}
private Mono<Void> saveChangeSessionId() {
if (!hasChangedSessionId()) {
return Mono.empty();
}
String sessionId = getId();
Publisher<Void> replaceSessionId = (s) -> {
this.originalSessionId = sessionId;
s.onComplete();
};
if (this.isNew) {
return Mono.from(replaceSessionId);
}
else {
String originalSessionKey = getSessionKey(this.originalSessionId);
String sessionKey = getSessionKey(sessionId);
return ReactiveRedisOperationsSessionRepository.this.sessionRedisOperations
.rename(originalSessionKey, sessionKey).and(replaceSessionId);
}
}
}
}

View File

@@ -0,0 +1,312 @@
/*
* Copyright 2014-2019 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
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.session.data.redis;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import org.springframework.data.redis.core.ReactiveRedisOperations;
import org.springframework.session.MapSession;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.util.Assert;
/**
* A {@link ReactiveSessionRepository} that is implemented using Spring Data's
* {@link ReactiveRedisOperations}.
*
* @author Vedran Pavic
* @since 2.2.0
*/
public class ReactiveRedisSessionRepository
implements ReactiveSessionRepository<ReactiveRedisSessionRepository.RedisSession> {
/**
* The default namespace for each key and channel in Redis used by Spring Session.
*/
public static final String DEFAULT_NAMESPACE = "spring:session";
private final ReactiveRedisOperations<String, Object> sessionRedisOperations;
/**
* The namespace for every key used by Spring Session in Redis.
*/
private String namespace = DEFAULT_NAMESPACE + ":";
/**
* If non-null, this value is used to override the default value for
* {@link RedisSession#setMaxInactiveInterval(Duration)}.
*/
private Integer defaultMaxInactiveInterval;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
/**
* Create a new {@link ReactiveRedisSessionRepository} instance.
* @param sessionRedisOperations the {@link ReactiveRedisOperations} to use for
* managing sessions
*/
public ReactiveRedisSessionRepository(ReactiveRedisOperations<String, Object> sessionRedisOperations) {
Assert.notNull(sessionRedisOperations, "sessionRedisOperations cannot be null");
this.sessionRedisOperations = sessionRedisOperations;
}
public void setRedisKeyNamespace(String namespace) {
Assert.hasText(namespace, "namespace cannot be null or empty");
this.namespace = namespace.trim() + ":";
}
/**
* Sets 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 number of seconds that the {@link Session}
* should be kept alive between client requests.
*/
public void setDefaultMaxInactiveInterval(int defaultMaxInactiveInterval) {
this.defaultMaxInactiveInterval = defaultMaxInactiveInterval;
}
/**
* Set the save mode.
* @param saveMode the save mode
*/
public void setSaveMode(SaveMode saveMode) {
Assert.notNull(saveMode, "saveMode must not be null");
this.saveMode = saveMode;
}
/**
* Returns the {@link ReactiveRedisOperations} used for sessions.
* @return the {@link ReactiveRedisOperations} used for sessions
*/
public ReactiveRedisOperations<String, Object> getSessionRedisOperations() {
return this.sessionRedisOperations;
}
@Override
public Mono<RedisSession> createSession() {
return Mono.defer(() -> {
MapSession cached = new MapSession();
if (this.defaultMaxInactiveInterval != null) {
cached.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
}
RedisSession session = new RedisSession(cached, true);
return Mono.just(session);
});
}
@Override
public Mono<Void> save(RedisSession session) {
if (session.isNew) {
return session.save();
}
String sessionKey = getSessionKey(session.hasChangedSessionId() ? session.originalSessionId : session.getId());
return this.sessionRedisOperations.hasKey(sessionKey).flatMap(
(exists) -> exists ? session.save() : Mono.error(new IllegalStateException("Session was invalidated")));
}
@Override
public Mono<RedisSession> findById(String id) {
String sessionKey = getSessionKey(id);
// @formatter:off
return this.sessionRedisOperations.opsForHash().entries(sessionKey)
.collectMap((e) -> e.getKey().toString(), Map.Entry::getValue)
.filter((map) -> !map.isEmpty())
.map(new RedisSessionMapper(id))
.filter((session) -> !session.isExpired())
.map((session) -> new RedisSession(session, false))
.switchIfEmpty(Mono.defer(() -> deleteById(id).then(Mono.empty())));
// @formatter:on
}
@Override
public Mono<Void> deleteById(String id) {
String sessionKey = getSessionKey(id);
return this.sessionRedisOperations.delete(sessionKey).then();
}
private static String getAttributeKey(String attributeName) {
return RedisSessionMapper.ATTRIBUTE_PREFIX + attributeName;
}
private String getSessionKey(String sessionId) {
return this.namespace + "sessions:" + sessionId;
}
/**
* A custom implementation of {@link Session} that uses a {@link MapSession} as the
* basis for its mapping. It keeps track of any attributes that have changed. When
* {@link RedisSession#saveDelta()} is invoked all the attributes that have been
* changed will be persisted.
*/
final class RedisSession implements Session {
private final MapSession cached;
private final Map<String, Object> delta = new HashMap<>();
private boolean isNew;
private String originalSessionId;
RedisSession(MapSession cached, boolean isNew) {
this.cached = cached;
this.isNew = isNew;
this.originalSessionId = cached.getId();
if (this.isNew) {
this.delta.put(RedisSessionMapper.CREATION_TIME_KEY, cached.getCreationTime().toEpochMilli());
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY,
(int) cached.getMaxInactiveInterval().getSeconds());
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, cached.getLastAccessedTime().toEpochMilli());
}
if (this.isNew || (ReactiveRedisSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
getAttributeNames().forEach((attributeName) -> this.delta.put(getAttributeKey(attributeName),
cached.getAttribute(attributeName)));
}
}
@Override
public String getId() {
return this.cached.getId();
}
@Override
public String changeSessionId() {
return this.cached.changeSessionId();
}
@Override
public <T> T getAttribute(String attributeName) {
T attributeValue = this.cached.getAttribute(attributeName);
if (attributeValue != null
&& ReactiveRedisSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
this.delta.put(getAttributeKey(attributeName), attributeValue);
}
return attributeValue;
}
@Override
public Set<String> getAttributeNames() {
return this.cached.getAttributeNames();
}
@Override
public void setAttribute(String attributeName, Object attributeValue) {
this.cached.setAttribute(attributeName, attributeValue);
this.delta.put(getAttributeKey(attributeName), attributeValue);
}
@Override
public void removeAttribute(String attributeName) {
this.cached.removeAttribute(attributeName);
this.delta.put(getAttributeKey(attributeName), null);
}
@Override
public Instant getCreationTime() {
return this.cached.getCreationTime();
}
@Override
public void setLastAccessedTime(Instant lastAccessedTime) {
this.cached.setLastAccessedTime(lastAccessedTime);
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, getLastAccessedTime().toEpochMilli());
}
@Override
public Instant getLastAccessedTime() {
return this.cached.getLastAccessedTime();
}
@Override
public void setMaxInactiveInterval(Duration interval) {
this.cached.setMaxInactiveInterval(interval);
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, (int) getMaxInactiveInterval().getSeconds());
}
@Override
public Duration getMaxInactiveInterval() {
return this.cached.getMaxInactiveInterval();
}
@Override
public boolean isExpired() {
return this.cached.isExpired();
}
private boolean hasChangedSessionId() {
return !getId().equals(this.originalSessionId);
}
private Mono<Void> save() {
return Mono.defer(() -> saveChangeSessionId().then(saveDelta()).doOnSuccess((aVoid) -> this.isNew = false));
}
private Mono<Void> saveDelta() {
if (this.delta.isEmpty()) {
return Mono.empty();
}
String sessionKey = getSessionKey(getId());
Mono<Boolean> update = ReactiveRedisSessionRepository.this.sessionRedisOperations.opsForHash()
.putAll(sessionKey, new HashMap<>(this.delta));
Mono<Boolean> setTtl = ReactiveRedisSessionRepository.this.sessionRedisOperations.expire(sessionKey,
getMaxInactiveInterval());
return update.and(setTtl).and((s) -> {
this.delta.clear();
s.onComplete();
}).then();
}
private Mono<Void> saveChangeSessionId() {
if (!hasChangedSessionId()) {
return Mono.empty();
}
String sessionId = getId();
Publisher<Void> replaceSessionId = (s) -> {
this.originalSessionId = sessionId;
s.onComplete();
};
if (this.isNew) {
return Mono.from(replaceSessionId);
}
else {
String originalSessionKey = getSessionKey(this.originalSessionId);
String sessionKey = getSessionKey(sessionId);
return ReactiveRedisSessionRepository.this.sessionRedisOperations.rename(originalSessionKey, sessionKey)
.and(replaceSessionId);
}
}
}
}

View File

@@ -0,0 +1,857 @@
/*
* Copyright 2014-2019 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
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.session.data.redis;
import java.time.Duration;
import java.time.Instant;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
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.core.NestedExceptionUtils;
import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.DelegatingIndexResolver;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.FlushMode;
import org.springframework.session.IndexResolver;
import org.springframework.session.MapSession;
import org.springframework.session.PrincipalNameIndexResolver;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDeletedEvent;
import org.springframework.session.events.SessionDestroyedEvent;
import org.springframework.session.events.SessionExpiredEvent;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.util.Assert;
/**
* <p>
* A {@link org.springframework.session.SessionRepository} that is implemented using
* Spring Data's {@link org.springframework.data.redis.core.RedisOperations}. In a web
* environment, this is typically used in combination with {@link SessionRepositoryFilter}
* . This implementation supports {@link SessionDeletedEvent} and
* {@link SessionExpiredEvent} by implementing {@link MessageListener}.
* </p>
*
* <h2>Creating a new instance</h2>
*
* A typical example of how to create a new instance can be seen below:
*
* <pre>
* RedisTemplate&lt;Object, Object&gt; redisTemplate = new RedisTemplate&lt;&gt;();
*
* // ... configure redisTemplate ...
*
* RedisIndexedSessionRepository redisSessionRepository =
* new RedisIndexedSessionRepository(redisTemplate);
* </pre>
*
* <p>
* For additional information on how to create a RedisTemplate, refer to the
* <a href = "https://docs.spring.io/spring-data/data-redis/docs/current/reference/html/"
* > Spring Data Redis Reference</a>.
* </p>
*
* <h2>Storage Details</h2>
*
* The sections below outline how Redis is updated for each operation. An example of
* creating a new session can be found below. The subsequent sections describe the
* details.
*
* <pre>
* HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 maxInactiveInterval 1800 lastAccessedTime 1404360000000 sessionAttr:attrName someAttrValue sessionAttr2:attrName someAttrValue2
* EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100
* APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe ""
* EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800
* SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
* EXPIRE spring:session:expirations1439245080000 2100
* </pre>
*
* <h3>Saving a Session</h3>
*
* <p>
* Each session is stored in Redis as a
* <a href="https://redis.io/topics/data-types#hashes">Hash</a>. Each session is set and
* updated using the <a href="https://redis.io/commands/hmset">HMSET command</a>. An
* example of how each session is stored can be seen below.
* </p>
*
* <pre>
* HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 maxInactiveInterval 1800 lastAccessedTime 1404360000000 sessionAttr:attrName someAttrValue sessionAttr:attrName2 someAttrValue2
* </pre>
*
* <p>
* In this example, the session following statements are true about the session:
* </p>
* <ul>
* <li>The session id is 33fdd1b6-b496-4b33-9f7d-df96679d32fe</li>
* <li>The session was created at 1404360000000 in milliseconds since midnight of 1/1/1970
* GMT.</li>
* <li>The session expires in 1800 seconds (30 minutes).</li>
* <li>The session was last accessed at 1404360000000 in milliseconds since midnight of
* 1/1/1970 GMT.</li>
* <li>The session has two attributes. The first is "attrName" with the value of
* "someAttrValue". The second session attribute is named "attrName2" with the value of
* "someAttrValue2".</li>
* </ul>
*
*
* <h3>Optimized Writes</h3>
*
* <p>
* The {@link RedisIndexedSessionRepository.RedisSession} keeps track of the properties
* that have changed and only updates those. This means if an attribute is written once
* and read many times we only need to write that attribute once. For example, assume the
* session attribute "sessionAttr2" from earlier was updated. The following would be
* executed upon saving:
* </p>
*
* <pre>
* HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe sessionAttr:attrName2 newValue
* </pre>
*
* <h3>SessionCreatedEvent</h3>
*
* <p>
* When a session is created an event is sent to Redis with the channel of
* "spring:session:channel:created:33fdd1b6-b496-4b33-9f7d-df96679d32fe" such that
* "33fdd1b6-b496-4b33-9f7d-df96679d32fe" is the sesion id. The body of the event will be
* the session that was created.
* </p>
*
* <p>
* If registered as a {@link MessageListener}, then {@link RedisIndexedSessionRepository}
* will then translate the Redis message into a {@link SessionCreatedEvent}.
* </p>
*
* <h3>Expiration</h3>
*
* <p>
* An expiration is associated to each session using the
* <a href="https://redis.io/commands/expire">EXPIRE command</a> based upon the
* {@link RedisIndexedSessionRepository.RedisSession#getMaxInactiveInterval()} . For
* example:
* </p>
*
* <pre>
* EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100
* </pre>
*
* <p>
* You will note that the expiration that is set is 5 minutes after the session actually
* expires. This is necessary so that the value of the session can be accessed when the
* session expires. An expiration is set on the session itself five minutes after it
* actually expires to ensure it is cleaned up, but only after we perform any necessary
* processing.
* </p>
*
* <p>
* <b>NOTE:</b> The {@link #findById(String)} method ensures that no expired sessions will
* be returned. This means there is no need to check the expiration before using a session
* </p>
*
* <p>
* Spring Session relies on the expired and delete
* <a href="https://redis.io/topics/notifications">keyspace notifications</a> from Redis
* to fire a SessionDestroyedEvent. It is the SessionDestroyedEvent that ensures resources
* associated with the Session are cleaned up. For example, when using Spring Session's
* WebSocket support the Redis expired or delete event is what triggers any WebSocket
* connections associated with the session to be closed.
* </p>
*
* <p>
* Expiration is not tracked directly on the session key itself since this would mean the
* session data would no longer be available. Instead a special session expires key is
* used. In our example the expires key is:
* </p>
*
* <pre>
* APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe ""
* EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800
* </pre>
*
* <p>
* When a session expires key is deleted or expires, the keyspace notification triggers a
* lookup of the actual session and a {@link SessionDestroyedEvent} is fired.
* </p>
*
* <p>
* One problem with relying on Redis expiration exclusively is that Redis makes no
* guarantee of when the expired event will be fired if the key has not been accessed.
* Specifically the background task that Redis uses to clean up expired keys is a low
* priority task and may not trigger the key expiration. For additional details see
* <a href="https://redis.io/topics/notifications">Timing of expired events</a> section in
* the Redis documentation.
* </p>
*
* <p>
* To circumvent the fact that expired events are not guaranteed to happen we can ensure
* that each key is accessed when it is expected to expire. This means that if the TTL is
* expired on the key, Redis will remove the key and fire the expired event when we try to
* access the key.
* </p>
*
* <p>
* For this reason, each session expiration is also tracked to the nearest minute. This
* allows a background task to access the potentially expired sessions to ensure that
* Redis expired events are fired in a more deterministic fashion. For example:
* </p>
*
* <pre>
* SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
* EXPIRE spring:session:expirations1439245080000 2100
* </pre>
*
* <p>
* The background task will then use these mappings to explicitly request each session
* expires key. By accessing the key, rather than deleting it, we ensure that Redis
* deletes the key for us only if the TTL is expired.
* </p>
* <p>
* <b>NOTE</b>: We do not explicitly delete the keys since in some instances there may be
* a race condition that incorrectly identifies a key as expired when it is not. Short of
* using distributed locks (which would kill our performance) there is no way to ensure
* the consistency of the expiration mapping. By simply accessing the key, we ensure that
* the key is only removed if the TTL on that key is expired.
* </p>
*
* @author Rob Winch
* @author Vedran Pavic
* @since 2.2.0
*/
public class RedisIndexedSessionRepository
implements FindByIndexNameSessionRepository<RedisIndexedSessionRepository.RedisSession>, MessageListener {
private static final Log logger = LogFactory.getLog(RedisIndexedSessionRepository.class);
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
/**
* The default Redis database used by Spring Session.
*/
public static final int DEFAULT_DATABASE = 0;
/**
* The default namespace for each key and channel in Redis used by Spring Session.
*/
public static final String DEFAULT_NAMESPACE = "spring:session";
private int database = DEFAULT_DATABASE;
/**
* The namespace for every key used by Spring Session in Redis.
*/
private String namespace = DEFAULT_NAMESPACE + ":";
private String sessionCreatedChannelPrefix;
private String sessionDeletedChannel;
private String sessionExpiredChannel;
private final RedisOperations<Object, Object> sessionRedisOperations;
private final RedisSessionExpirationPolicy expirationPolicy;
private ApplicationEventPublisher eventPublisher = (event) -> {
};
/**
* If non-null, this value is used to override the default value for
* {@link RedisSession#setMaxInactiveInterval(Duration)}.
*/
private Integer defaultMaxInactiveInterval;
private IndexResolver<Session> indexResolver = new DelegatingIndexResolver<>(new PrincipalNameIndexResolver<>());
private RedisSerializer<Object> defaultSerializer = new JdkSerializationRedisSerializer();
private FlushMode flushMode = FlushMode.ON_SAVE;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
/**
* Creates a new instance. For an example, refer to the class level javadoc.
* @param sessionRedisOperations the {@link RedisOperations} to use for managing the
* sessions. Cannot be null.
*/
public RedisIndexedSessionRepository(RedisOperations<Object, Object> sessionRedisOperations) {
Assert.notNull(sessionRedisOperations, "sessionRedisOperations cannot be null");
this.sessionRedisOperations = sessionRedisOperations;
this.expirationPolicy = new RedisSessionExpirationPolicy(sessionRedisOperations, this::getExpirationsKey,
this::getSessionKey);
configureSessionChannels();
}
/**
* Sets the {@link ApplicationEventPublisher} that is used to publish
* {@link SessionDestroyedEvent}. The default is to not publish a
* {@link SessionDestroyedEvent}.
* @param applicationEventPublisher the {@link ApplicationEventPublisher} that is used
* to publish {@link SessionDestroyedEvent}. Cannot be null.
*/
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
Assert.notNull(applicationEventPublisher, "applicationEventPublisher cannot be null");
this.eventPublisher = applicationEventPublisher;
}
/**
* Sets 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 number of seconds that the {@link Session}
* should be kept alive between client requests.
*/
public void setDefaultMaxInactiveInterval(int defaultMaxInactiveInterval) {
this.defaultMaxInactiveInterval = defaultMaxInactiveInterval;
}
/**
* Set the {@link IndexResolver} to use.
* @param indexResolver the index resolver
*/
public void setIndexResolver(IndexResolver<Session> indexResolver) {
Assert.notNull(indexResolver, "indexResolver cannot be null");
this.indexResolver = indexResolver;
}
/**
* Sets the default redis serializer. Replaces default serializer which is based on
* {@link JdkSerializationRedisSerializer}.
* @param defaultSerializer the new default redis serializer
*/
public void setDefaultSerializer(RedisSerializer<Object> defaultSerializer) {
Assert.notNull(defaultSerializer, "defaultSerializer cannot be null");
this.defaultSerializer = defaultSerializer;
}
/**
* Sets the redis flush mode. Default flush mode is {@link FlushMode#ON_SAVE}.
* @param flushMode the flush mode
*/
public void setFlushMode(FlushMode flushMode) {
Assert.notNull(flushMode, "flushMode cannot be null");
this.flushMode = flushMode;
}
/**
* Set the save mode.
* @param saveMode the save mode
*/
public void setSaveMode(SaveMode saveMode) {
Assert.notNull(saveMode, "saveMode must not be null");
this.saveMode = saveMode;
}
/**
* Sets the database index to use. Defaults to {@link #DEFAULT_DATABASE}.
* @param database the database index to use
*/
public void setDatabase(int database) {
this.database = database;
configureSessionChannels();
}
private void configureSessionChannels() {
this.sessionCreatedChannelPrefix = this.namespace + "event:" + this.database + ":created:";
this.sessionDeletedChannel = "__keyevent@" + this.database + "__:del";
this.sessionExpiredChannel = "__keyevent@" + this.database + "__:expired";
}
/**
* Returns the {@link RedisOperations} used for sessions.
* @return the {@link RedisOperations} used for sessions
*/
public RedisOperations<Object, Object> getSessionRedisOperations() {
return this.sessionRedisOperations;
}
@Override
public void save(RedisSession session) {
session.save();
if (session.isNew) {
String sessionCreatedKey = getSessionCreatedChannel(session.getId());
this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);
session.isNew = false;
}
}
public void cleanupExpiredSessions() {
this.expirationPolicy.cleanExpiredSessions();
}
@Override
public RedisSession findById(String id) {
return getSession(id, false);
}
@Override
public Map<String, RedisSession> findByIndexNameAndIndexValue(String indexName, String indexValue) {
if (!PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) {
return Collections.emptyMap();
}
String principalKey = getPrincipalKey(indexValue);
Set<Object> sessionIds = this.sessionRedisOperations.boundSetOps(principalKey).members();
Map<String, RedisSession> sessions = new HashMap<>(sessionIds.size());
for (Object id : sessionIds) {
RedisSession session = findById((String) id);
if (session != null) {
sessions.put(session.getId(), session);
}
}
return sessions;
}
/**
* Gets the session.
* @param id the session id
* @param allowExpired if true, will also include expired sessions that have not been
* deleted. If false, will ensure expired sessions are not returned.
* @return the Redis session
*/
private RedisSession getSession(String id, boolean allowExpired) {
Map<Object, Object> entries = getSessionBoundHashOperations(id).entries();
if (entries.isEmpty()) {
return null;
}
MapSession loaded = loadSession(id, entries);
if (!allowExpired && loaded.isExpired()) {
return null;
}
RedisSession result = new RedisSession(loaded, false);
result.originalLastAccessTime = loaded.getLastAccessedTime();
return result;
}
private MapSession loadSession(String id, Map<Object, Object> entries) {
MapSession loaded = new MapSession(id);
for (Map.Entry<Object, Object> entry : entries.entrySet()) {
String key = (String) entry.getKey();
if (RedisSessionMapper.CREATION_TIME_KEY.equals(key)) {
loaded.setCreationTime(Instant.ofEpochMilli((long) entry.getValue()));
}
else if (RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY.equals(key)) {
loaded.setMaxInactiveInterval(Duration.ofSeconds((int) entry.getValue()));
}
else if (RedisSessionMapper.LAST_ACCESSED_TIME_KEY.equals(key)) {
loaded.setLastAccessedTime(Instant.ofEpochMilli((long) entry.getValue()));
}
else if (key.startsWith(RedisSessionMapper.ATTRIBUTE_PREFIX)) {
loaded.setAttribute(key.substring(RedisSessionMapper.ATTRIBUTE_PREFIX.length()), entry.getValue());
}
}
return loaded;
}
@Override
public void deleteById(String sessionId) {
RedisSession session = getSession(sessionId, true);
if (session == null) {
return;
}
cleanupPrincipalIndex(session);
this.expirationPolicy.onDelete(session);
String expireKey = getExpiredKey(session.getId());
this.sessionRedisOperations.delete(expireKey);
session.setMaxInactiveInterval(Duration.ZERO);
save(session);
}
@Override
public RedisSession createSession() {
MapSession cached = new MapSession();
if (this.defaultMaxInactiveInterval != null) {
cached.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
}
RedisSession session = new RedisSession(cached, true);
session.flushImmediateIfNecessary();
return session;
}
@Override
public void onMessage(Message message, byte[] pattern) {
byte[] messageChannel = message.getChannel();
byte[] messageBody = message.getBody();
String channel = new String(messageChannel);
if (channel.startsWith(this.sessionCreatedChannelPrefix)) {
// TODO: is this thread safe?
@SuppressWarnings("unchecked")
Map<Object, Object> loaded = (Map<Object, Object>) this.defaultSerializer.deserialize(message.getBody());
handleCreated(loaded, channel);
return;
}
String body = new String(messageBody);
if (!body.startsWith(getExpiredKeyPrefix())) {
return;
}
boolean isDeleted = channel.equals(this.sessionDeletedChannel);
if (isDeleted || channel.equals(this.sessionExpiredChannel)) {
int beginIndex = body.lastIndexOf(":") + 1;
int endIndex = body.length();
String sessionId = body.substring(beginIndex, endIndex);
RedisSession session = getSession(sessionId, true);
if (session == null) {
logger.warn("Unable to publish SessionDestroyedEvent for session " + sessionId);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Publishing SessionDestroyedEvent for session " + sessionId);
}
cleanupPrincipalIndex(session);
if (isDeleted) {
handleDeleted(session);
}
else {
handleExpired(session);
}
}
}
private void cleanupPrincipalIndex(RedisSession session) {
String sessionId = session.getId();
Map<String, String> indexes = RedisIndexedSessionRepository.this.indexResolver.resolveIndexesFor(session);
String principal = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
if (principal != null) {
this.sessionRedisOperations.boundSetOps(getPrincipalKey(principal)).remove(sessionId);
}
}
private void handleCreated(Map<Object, Object> loaded, String channel) {
String id = channel.substring(channel.lastIndexOf(":") + 1);
Session session = loadSession(id, loaded);
publishEvent(new SessionCreatedEvent(this, session));
}
private void handleDeleted(RedisSession session) {
publishEvent(new SessionDeletedEvent(this, session));
}
private void handleExpired(RedisSession session) {
publishEvent(new SessionExpiredEvent(this, session));
}
private void publishEvent(ApplicationEvent event) {
try {
this.eventPublisher.publishEvent(event);
}
catch (Throwable ex) {
logger.error("Error publishing " + event + ".", ex);
}
}
public void setRedisKeyNamespace(String namespace) {
Assert.hasText(namespace, "namespace cannot be null or empty");
this.namespace = namespace.trim() + ":";
configureSessionChannels();
}
/**
* Gets the Hash key for this session by prefixing it appropriately.
* @param sessionId the session id
* @return the Hash key for this session by prefixing it appropriately.
*/
String getSessionKey(String sessionId) {
return this.namespace + "sessions:" + sessionId;
}
String getPrincipalKey(String principalName) {
return this.namespace + "index:" + FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME + ":"
+ principalName;
}
String getExpirationsKey(long expiration) {
return this.namespace + "expirations:" + expiration;
}
private String getExpiredKey(String sessionId) {
return getExpiredKeyPrefix() + sessionId;
}
private String getSessionCreatedChannel(String sessionId) {
return getSessionCreatedChannelPrefix() + sessionId;
}
private String getExpiredKeyPrefix() {
return this.namespace + "sessions:expires:";
}
/**
* Gets the prefix for the channel that {@link SessionCreatedEvent}s are published to.
* The suffix is the session id of the session that was created.
* @return the prefix for the channel that {@link SessionCreatedEvent}s are published
* to
*/
public String getSessionCreatedChannelPrefix() {
return this.sessionCreatedChannelPrefix;
}
/**
* Gets the name of the channel that {@link SessionDeletedEvent}s are published to.
* @return the name for the channel that {@link SessionDeletedEvent}s are published to
*/
public String getSessionDeletedChannel() {
return this.sessionDeletedChannel;
}
/**
* Gets the name of the channel that {@link SessionExpiredEvent}s are published to.
* @return the name for the channel that {@link SessionExpiredEvent}s are published to
*/
public String getSessionExpiredChannel() {
return this.sessionExpiredChannel;
}
/**
* Gets the {@link BoundHashOperations} to operate on a {@link Session}.
* @param sessionId the id of the {@link Session} to work with
* @return the {@link BoundHashOperations} to operate on a {@link Session}
*/
private BoundHashOperations<Object, Object, Object> getSessionBoundHashOperations(String sessionId) {
String key = getSessionKey(sessionId);
return this.sessionRedisOperations.boundHashOps(key);
}
/**
* Gets the key for the specified session attribute.
* @param attributeName the attribute name
* @return the attribute key name
*/
static String getSessionAttrNameKey(String attributeName) {
return RedisSessionMapper.ATTRIBUTE_PREFIX + attributeName;
}
/**
* A custom implementation of {@link Session} that uses a {@link MapSession} as the
* basis for its mapping. It keeps track of any attributes that have changed. When
* {@link RedisIndexedSessionRepository.RedisSession#saveDelta()} is invoked all the
* attributes that have been changed will be persisted.
*
* @author Rob Winch
*/
final class RedisSession implements Session {
private final MapSession cached;
private Instant originalLastAccessTime;
private Map<String, Object> delta = new HashMap<>();
private boolean isNew;
private String originalPrincipalName;
private String originalSessionId;
RedisSession(MapSession cached, boolean isNew) {
this.cached = cached;
this.isNew = isNew;
this.originalSessionId = cached.getId();
Map<String, String> indexes = RedisIndexedSessionRepository.this.indexResolver.resolveIndexesFor(this);
this.originalPrincipalName = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
if (this.isNew) {
this.delta.put(RedisSessionMapper.CREATION_TIME_KEY, cached.getCreationTime().toEpochMilli());
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY,
(int) cached.getMaxInactiveInterval().getSeconds());
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, cached.getLastAccessedTime().toEpochMilli());
}
if (this.isNew || (RedisIndexedSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
getAttributeNames().forEach((attributeName) -> this.delta.put(getSessionAttrNameKey(attributeName),
cached.getAttribute(attributeName)));
}
}
@Override
public void setLastAccessedTime(Instant lastAccessedTime) {
this.cached.setLastAccessedTime(lastAccessedTime);
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, getLastAccessedTime().toEpochMilli());
flushImmediateIfNecessary();
}
@Override
public boolean isExpired() {
return this.cached.isExpired();
}
@Override
public Instant getCreationTime() {
return this.cached.getCreationTime();
}
@Override
public String getId() {
return this.cached.getId();
}
@Override
public String changeSessionId() {
return this.cached.changeSessionId();
}
@Override
public Instant getLastAccessedTime() {
return this.cached.getLastAccessedTime();
}
@Override
public void setMaxInactiveInterval(Duration interval) {
this.cached.setMaxInactiveInterval(interval);
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, (int) getMaxInactiveInterval().getSeconds());
flushImmediateIfNecessary();
}
@Override
public Duration getMaxInactiveInterval() {
return this.cached.getMaxInactiveInterval();
}
@Override
public <T> T getAttribute(String attributeName) {
T attributeValue = this.cached.getAttribute(attributeName);
if (attributeValue != null
&& RedisIndexedSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
this.delta.put(getSessionAttrNameKey(attributeName), attributeValue);
}
return attributeValue;
}
@Override
public Set<String> getAttributeNames() {
return this.cached.getAttributeNames();
}
@Override
public void setAttribute(String attributeName, Object attributeValue) {
this.cached.setAttribute(attributeName, attributeValue);
this.delta.put(getSessionAttrNameKey(attributeName), attributeValue);
flushImmediateIfNecessary();
}
@Override
public void removeAttribute(String attributeName) {
this.cached.removeAttribute(attributeName);
this.delta.put(getSessionAttrNameKey(attributeName), null);
flushImmediateIfNecessary();
}
private void flushImmediateIfNecessary() {
if (RedisIndexedSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {
save();
}
}
private void save() {
saveChangeSessionId();
saveDelta();
}
/**
* Saves any attributes that have been changed and updates the expiration of this
* session.
*/
private void saveDelta() {
if (this.delta.isEmpty()) {
return;
}
String sessionId = getId();
getSessionBoundHashOperations(sessionId).putAll(this.delta);
String principalSessionKey = getSessionAttrNameKey(
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
String securityPrincipalSessionKey = getSessionAttrNameKey(SPRING_SECURITY_CONTEXT);
if (this.delta.containsKey(principalSessionKey) || this.delta.containsKey(securityPrincipalSessionKey)) {
if (this.originalPrincipalName != null) {
String originalPrincipalRedisKey = getPrincipalKey(this.originalPrincipalName);
RedisIndexedSessionRepository.this.sessionRedisOperations.boundSetOps(originalPrincipalRedisKey)
.remove(sessionId);
}
Map<String, String> indexes = RedisIndexedSessionRepository.this.indexResolver.resolveIndexesFor(this);
String principal = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
this.originalPrincipalName = principal;
if (principal != null) {
String principalRedisKey = getPrincipalKey(principal);
RedisIndexedSessionRepository.this.sessionRedisOperations.boundSetOps(principalRedisKey)
.add(sessionId);
}
}
this.delta = new HashMap<>(this.delta.size());
Long originalExpiration = (this.originalLastAccessTime != null)
? this.originalLastAccessTime.plus(getMaxInactiveInterval()).toEpochMilli() : null;
RedisIndexedSessionRepository.this.expirationPolicy.onExpirationUpdated(originalExpiration, this);
}
private void saveChangeSessionId() {
String sessionId = getId();
if (sessionId.equals(this.originalSessionId)) {
return;
}
if (!this.isNew) {
String originalSessionIdKey = getSessionKey(this.originalSessionId);
String sessionIdKey = getSessionKey(sessionId);
try {
RedisIndexedSessionRepository.this.sessionRedisOperations.rename(originalSessionIdKey,
sessionIdKey);
}
catch (NonTransientDataAccessException ex) {
handleErrNoSuchKeyError(ex);
}
String originalExpiredKey = getExpiredKey(this.originalSessionId);
String expiredKey = getExpiredKey(sessionId);
try {
RedisIndexedSessionRepository.this.sessionRedisOperations.rename(originalExpiredKey, expiredKey);
}
catch (NonTransientDataAccessException ex) {
handleErrNoSuchKeyError(ex);
}
}
this.originalSessionId = sessionId;
}
private void handleErrNoSuchKeyError(NonTransientDataAccessException ex) {
if (!"ERR no such key".equals(NestedExceptionUtils.getMostSpecificCause(ex).getMessage())) {
throw ex;
}
}
}
}

View File

@@ -16,334 +16,31 @@
package org.springframework.session.data.redis;
import java.time.Duration;
import java.time.Instant;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
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.core.NestedExceptionUtils;
import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.DelegatingIndexResolver;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.FlushMode;
import org.springframework.session.IndexResolver;
import org.springframework.session.MapSession;
import org.springframework.session.PrincipalNameIndexResolver;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDeletedEvent;
import org.springframework.session.events.SessionDestroyedEvent;
import org.springframework.session.events.SessionExpiredEvent;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.session.SessionRepository;
import org.springframework.util.Assert;
/**
* <p>
* A {@link org.springframework.session.SessionRepository} that is implemented using
* Spring Data's {@link org.springframework.data.redis.core.RedisOperations}. In a web
* environment, this is typically used in combination with {@link SessionRepositoryFilter}
* . This implementation supports {@link SessionDeletedEvent} and
* {@link SessionExpiredEvent} by implementing {@link MessageListener}.
* </p>
*
* <h2>Creating a new instance</h2>
*
* A typical example of how to create a new instance can be seen below:
*
* <pre>
* RedisTemplate&lt;Object, Object&gt; redisTemplate = new RedisTemplate&lt;&gt;();
*
* // ... configure redisTemplate ...
*
* RedisOperationsSessionRepository redisSessionRepository =
* new RedisOperationsSessionRepository(redisTemplate);
* </pre>
*
* <p>
* For additional information on how to create a RedisTemplate, refer to the
* <a href = "https://docs.spring.io/spring-data/data-redis/docs/current/reference/html/"
* > Spring Data Redis Reference</a>.
* </p>
*
* <h2>Storage Details</h2>
*
* The sections below outline how Redis is updated for each operation. An example of
* creating a new session can be found below. The subsequent sections describe the
* details.
*
* <pre>
* HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 maxInactiveInterval 1800 lastAccessedTime 1404360000000 sessionAttr:attrName someAttrValue sessionAttr2:attrName someAttrValue2
* EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100
* APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe ""
* EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800
* SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
* EXPIRE spring:session:expirations1439245080000 2100
* </pre>
*
* <h3>Saving a Session</h3>
*
* <p>
* Each session is stored in Redis as a
* <a href="https://redis.io/topics/data-types#hashes">Hash</a>. Each session is set and
* updated using the <a href="https://redis.io/commands/hmset">HMSET command</a>. An
* example of how each session is stored can be seen below.
* </p>
*
* <pre>
* HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 maxInactiveInterval 1800 lastAccessedTime 1404360000000 sessionAttr:attrName someAttrValue sessionAttr:attrName2 someAttrValue2
* </pre>
*
* <p>
* In this example, the session following statements are true about the session:
* </p>
* <ul>
* <li>The session id is 33fdd1b6-b496-4b33-9f7d-df96679d32fe</li>
* <li>The session was created at 1404360000000 in milliseconds since midnight of 1/1/1970
* GMT.</li>
* <li>The session expires in 1800 seconds (30 minutes).</li>
* <li>The session was last accessed at 1404360000000 in milliseconds since midnight of
* 1/1/1970 GMT.</li>
* <li>The session has two attributes. The first is "attrName" with the value of
* "someAttrValue". The second session attribute is named "attrName2" with the value of
* "someAttrValue2".</li>
* </ul>
*
*
* <h3>Optimized Writes</h3>
*
* <p>
* The
* {@link org.springframework.session.data.redis.RedisOperationsSessionRepository.RedisSession}
* keeps track of the properties that have changed and only updates those. This means if
* an attribute is written once and read many times we only need to write that attribute
* once. For example, assume the session attribute "sessionAttr2" from earlier was
* updated. The following would be executed upon saving:
* </p>
*
* <pre>
* HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe sessionAttr:attrName2 newValue
* </pre>
*
* <h3>SessionCreatedEvent</h3>
*
* <p>
* When a session is created an event is sent to Redis with the channel of
* "spring:session:channel:created:33fdd1b6-b496-4b33-9f7d-df96679d32fe" such that
* "33fdd1b6-b496-4b33-9f7d-df96679d32fe" is the sesion id. The body of the event will be
* the session that was created.
* </p>
*
* <p>
* If registered as a {@link MessageListener}, then
* {@link RedisOperationsSessionRepository} will then translate the Redis message into a
* {@link SessionCreatedEvent}.
* </p>
*
* <h3>Expiration</h3>
*
* <p>
* An expiration is associated to each session using the
* <a href="https://redis.io/commands/expire">EXPIRE command</a> based upon the
* {@link org.springframework.session.data.redis.RedisOperationsSessionRepository.RedisSession#getMaxInactiveInterval()}
* . For example:
* </p>
*
* <pre>
* EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100
* </pre>
*
* <p>
* You will note that the expiration that is set is 5 minutes after the session actually
* expires. This is necessary so that the value of the session can be accessed when the
* session expires. An expiration is set on the session itself five minutes after it
* actually expires to ensure it is cleaned up, but only after we perform any necessary
* processing.
* </p>
*
* <p>
* <b>NOTE:</b> The {@link #findById(String)} method ensures that no expired sessions will
* be returned. This means there is no need to check the expiration before using a session
* </p>
*
* <p>
* Spring Session relies on the expired and delete
* <a href="https://redis.io/topics/notifications">keyspace notifications</a> from Redis
* to fire a SessionDestroyedEvent. It is the SessionDestroyedEvent that ensures resources
* associated with the Session are cleaned up. For example, when using Spring Session's
* WebSocket support the Redis expired or delete event is what triggers any WebSocket
* connections associated with the session to be closed.
* </p>
*
* <p>
* Expiration is not tracked directly on the session key itself since this would mean the
* session data would no longer be available. Instead a special session expires key is
* used. In our example the expires key is:
* </p>
*
* <pre>
* APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe ""
* EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800
* </pre>
*
* <p>
* When a session expires key is deleted or expires, the keyspace notification triggers a
* lookup of the actual session and a {@link SessionDestroyedEvent} is fired.
* </p>
*
* <p>
* One problem with relying on Redis expiration exclusively is that Redis makes no
* guarantee of when the expired event will be fired if the key has not been accessed.
* Specifically the background task that Redis uses to clean up expired keys is a low
* priority task and may not trigger the key expiration. For additional details see
* <a href="https://redis.io/topics/notifications">Timing of expired events</a> section in
* the Redis documentation.
* </p>
*
* <p>
* To circumvent the fact that expired events are not guaranteed to happen we can ensure
* that each key is accessed when it is expected to expire. This means that if the TTL is
* expired on the key, Redis will remove the key and fire the expired event when we try to
* access the key.
* </p>
*
* <p>
* For this reason, each session expiration is also tracked to the nearest minute. This
* allows a background task to access the potentially expired sessions to ensure that
* Redis expired events are fired in a more deterministic fashion. For example:
* </p>
*
* <pre>
* SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
* EXPIRE spring:session:expirations1439245080000 2100
* </pre>
*
* <p>
* The background task will then use these mappings to explicitly request each session
* expires key. By accessing the key, rather than deleting it, we ensure that Redis
* deletes the key for us only if the TTL is expired.
* </p>
* <p>
* <b>NOTE</b>: We do not explicitly delete the keys since in some instances there may be
* a race condition that incorrectly identifies a key as expired when it is not. Short of
* using distributed locks (which would kill our performance) there is no way to ensure
* the consistency of the expiration mapping. By simply accessing the key, we ensure that
* the key is only removed if the TTL on that key is expired.
* </p>
* This {@link SessionRepository} implementation is kept in order to support migration to
* {@link RedisIndexedSessionRepository} in a backwards compatible manner.
*
* @author Rob Winch
* @author Vedran Pavic
* @since 1.0
* @deprecated since 2.2.0 in favor of {@link RedisIndexedSessionRepository}
*/
public class RedisOperationsSessionRepository
implements FindByIndexNameSessionRepository<RedisOperationsSessionRepository.RedisSession>, MessageListener {
private static final Log logger = LogFactory.getLog(RedisOperationsSessionRepository.class);
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
/**
* The default Redis database used by Spring Session.
*/
public static final int DEFAULT_DATABASE = 0;
/**
* The default namespace for each key and channel in Redis used by Spring Session.
*/
public static final String DEFAULT_NAMESPACE = "spring:session";
private int database = RedisOperationsSessionRepository.DEFAULT_DATABASE;
/**
* The namespace for every key used by Spring Session in Redis.
*/
private String namespace = DEFAULT_NAMESPACE + ":";
private String sessionCreatedChannelPrefix;
private String sessionDeletedChannel;
private String sessionExpiredChannel;
private final RedisOperations<Object, Object> sessionRedisOperations;
private final RedisSessionExpirationPolicy expirationPolicy;
private final IndexResolver<RedisSession> indexResolver;
private ApplicationEventPublisher eventPublisher = (event) -> {
};
/**
* If non-null, this value is used to override the default value for
* {@link RedisSession#setMaxInactiveInterval(Duration)}.
*/
private Integer defaultMaxInactiveInterval;
private RedisSerializer<Object> defaultSerializer = new JdkSerializationRedisSerializer();
private FlushMode flushMode = FlushMode.ON_SAVE;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
@Deprecated
public class RedisOperationsSessionRepository extends RedisIndexedSessionRepository {
/**
* Creates a new instance. For an example, refer to the class level javadoc.
* @param sessionRedisOperations the {@link RedisOperations} to use for managing the
* sessions. Cannot be null.
* @see RedisIndexedSessionRepository#RedisIndexedSessionRepository(RedisOperations)
*/
public RedisOperationsSessionRepository(RedisOperations<Object, Object> sessionRedisOperations) {
Assert.notNull(sessionRedisOperations, "sessionRedisOperations cannot be null");
this.sessionRedisOperations = sessionRedisOperations;
this.expirationPolicy = new RedisSessionExpirationPolicy(sessionRedisOperations, this::getExpirationsKey,
this::getSessionKey);
this.indexResolver = new DelegatingIndexResolver<>(new PrincipalNameIndexResolver<>());
configureSessionChannels();
}
/**
* Sets the {@link ApplicationEventPublisher} that is used to publish
* {@link SessionDestroyedEvent}. The default is to not publish a
* {@link SessionDestroyedEvent}.
* @param applicationEventPublisher the {@link ApplicationEventPublisher} that is used
* to publish {@link SessionDestroyedEvent}. Cannot be null.
*/
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
Assert.notNull(applicationEventPublisher, "applicationEventPublisher cannot be null");
this.eventPublisher = applicationEventPublisher;
}
/**
* Sets 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 number of seconds that the {@link Session}
* should be kept alive between client requests.
*/
public void setDefaultMaxInactiveInterval(int defaultMaxInactiveInterval) {
this.defaultMaxInactiveInterval = defaultMaxInactiveInterval;
}
/**
* Sets the default redis serializer. Replaces default serializer which is based on
* {@link JdkSerializationRedisSerializer}.
* @param defaultSerializer the new default redis serializer
*/
public void setDefaultSerializer(RedisSerializer<Object> defaultSerializer) {
Assert.notNull(defaultSerializer, "defaultSerializer cannot be null");
this.defaultSerializer = defaultSerializer;
super(sessionRedisOperations);
}
/**
@@ -357,517 +54,4 @@ public class RedisOperationsSessionRepository
setFlushMode(redisFlushMode.getFlushMode());
}
/**
* Sets the redis flush mode. Default flush mode is {@link FlushMode#ON_SAVE}.
* @param flushMode the flush mode
*/
public void setFlushMode(FlushMode flushMode) {
Assert.notNull(flushMode, "flushMode cannot be null");
this.flushMode = flushMode;
}
/**
* Set the save mode.
* @param saveMode the save mode
*/
public void setSaveMode(SaveMode saveMode) {
Assert.notNull(saveMode, "saveMode must not be null");
this.saveMode = saveMode;
}
/**
* Sets the database index to use. Defaults to {@link #DEFAULT_DATABASE}.
* @param database the database index to use
*/
public void setDatabase(int database) {
this.database = database;
configureSessionChannels();
}
private void configureSessionChannels() {
this.sessionCreatedChannelPrefix = this.namespace + "event:" + this.database + ":created:";
this.sessionDeletedChannel = "__keyevent@" + this.database + "__:del";
this.sessionExpiredChannel = "__keyevent@" + this.database + "__:expired";
}
/**
* Returns the {@link RedisOperations} used for sessions.
* @return the {@link RedisOperations} used for sessions
* @since 2.0.0
*/
public RedisOperations<Object, Object> getSessionRedisOperations() {
return this.sessionRedisOperations;
}
@Override
public void save(RedisSession session) {
session.save();
if (session.isNew()) {
String sessionCreatedKey = getSessionCreatedChannel(session.getId());
this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);
session.setNew(false);
}
}
public void cleanupExpiredSessions() {
this.expirationPolicy.cleanExpiredSessions();
}
@Override
public RedisSession findById(String id) {
return getSession(id, false);
}
@Override
public Map<String, RedisSession> findByIndexNameAndIndexValue(String indexName, String indexValue) {
if (!PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) {
return Collections.emptyMap();
}
String principalKey = getPrincipalKey(indexValue);
Set<Object> sessionIds = this.sessionRedisOperations.boundSetOps(principalKey).members();
Map<String, RedisSession> sessions = new HashMap<>(sessionIds.size());
for (Object id : sessionIds) {
RedisSession session = findById((String) id);
if (session != null) {
sessions.put(session.getId(), session);
}
}
return sessions;
}
/**
* Gets the session.
* @param id the session id
* @param allowExpired if true, will also include expired sessions that have not been
* deleted. If false, will ensure expired sessions are not returned.
* @return the Redis session
*/
private RedisSession getSession(String id, boolean allowExpired) {
Map<Object, Object> entries = getSessionBoundHashOperations(id).entries();
if (entries.isEmpty()) {
return null;
}
MapSession loaded = loadSession(id, entries);
if (!allowExpired && loaded.isExpired()) {
return null;
}
RedisSession result = new RedisSession(loaded, false);
result.originalLastAccessTime = loaded.getLastAccessedTime();
return result;
}
private MapSession loadSession(String id, Map<Object, Object> entries) {
MapSession loaded = new MapSession(id);
for (Map.Entry<Object, Object> entry : entries.entrySet()) {
String key = (String) entry.getKey();
if (RedisSessionMapper.CREATION_TIME_KEY.equals(key)) {
loaded.setCreationTime(Instant.ofEpochMilli((long) entry.getValue()));
}
else if (RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY.equals(key)) {
loaded.setMaxInactiveInterval(Duration.ofSeconds((int) entry.getValue()));
}
else if (RedisSessionMapper.LAST_ACCESSED_TIME_KEY.equals(key)) {
loaded.setLastAccessedTime(Instant.ofEpochMilli((long) entry.getValue()));
}
else if (key.startsWith(RedisSessionMapper.ATTRIBUTE_PREFIX)) {
loaded.setAttribute(key.substring(RedisSessionMapper.ATTRIBUTE_PREFIX.length()), entry.getValue());
}
}
return loaded;
}
@Override
public void deleteById(String sessionId) {
RedisSession session = getSession(sessionId, true);
if (session == null) {
return;
}
cleanupPrincipalIndex(session);
this.expirationPolicy.onDelete(session);
String expireKey = getExpiredKey(session.getId());
this.sessionRedisOperations.delete(expireKey);
session.setMaxInactiveInterval(Duration.ZERO);
save(session);
}
@Override
public RedisSession createSession() {
MapSession cached = new MapSession();
if (this.defaultMaxInactiveInterval != null) {
cached.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
}
RedisSession session = new RedisSession(cached, true);
session.flushImmediateIfNecessary();
return session;
}
@Override
@SuppressWarnings("unchecked")
public void onMessage(Message message, byte[] pattern) {
byte[] messageChannel = message.getChannel();
byte[] messageBody = message.getBody();
String channel = new String(messageChannel);
if (channel.startsWith(this.sessionCreatedChannelPrefix)) {
// TODO: is this thread safe?
Map<Object, Object> loaded = (Map<Object, Object>) this.defaultSerializer.deserialize(message.getBody());
handleCreated(loaded, channel);
return;
}
String body = new String(messageBody);
if (!body.startsWith(getExpiredKeyPrefix())) {
return;
}
boolean isDeleted = channel.equals(this.sessionDeletedChannel);
if (isDeleted || channel.equals(this.sessionExpiredChannel)) {
int beginIndex = body.lastIndexOf(":") + 1;
int endIndex = body.length();
String sessionId = body.substring(beginIndex, endIndex);
RedisSession session = getSession(sessionId, true);
if (session == null) {
logger.warn("Unable to publish SessionDestroyedEvent for session " + sessionId);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Publishing SessionDestroyedEvent for session " + sessionId);
}
cleanupPrincipalIndex(session);
if (isDeleted) {
handleDeleted(session);
}
else {
handleExpired(session);
}
}
}
private void cleanupPrincipalIndex(RedisSession session) {
String sessionId = session.getId();
Map<String, String> indexes = RedisOperationsSessionRepository.this.indexResolver.resolveIndexesFor(session);
String principal = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
if (principal != null) {
this.sessionRedisOperations.boundSetOps(getPrincipalKey(principal)).remove(sessionId);
}
}
private void handleCreated(Map<Object, Object> loaded, String channel) {
String id = channel.substring(channel.lastIndexOf(":") + 1);
Session session = loadSession(id, loaded);
publishEvent(new SessionCreatedEvent(this, session));
}
private void handleDeleted(RedisSession session) {
publishEvent(new SessionDeletedEvent(this, session));
}
private void handleExpired(RedisSession session) {
publishEvent(new SessionExpiredEvent(this, session));
}
private void publishEvent(ApplicationEvent event) {
try {
this.eventPublisher.publishEvent(event);
}
catch (Throwable ex) {
logger.error("Error publishing " + event + ".", ex);
}
}
public void setRedisKeyNamespace(String namespace) {
Assert.hasText(namespace, "namespace cannot be null or empty");
this.namespace = namespace.trim() + ":";
configureSessionChannels();
}
/**
* Gets the Hash key for this session by prefixing it appropriately.
* @param sessionId the session id
* @return the Hash key for this session by prefixing it appropriately.
*/
String getSessionKey(String sessionId) {
return this.namespace + "sessions:" + sessionId;
}
String getPrincipalKey(String principalName) {
return this.namespace + "index:" + FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME + ":"
+ principalName;
}
String getExpirationsKey(long expiration) {
return this.namespace + "expirations:" + expiration;
}
private String getExpiredKey(String sessionId) {
return getExpiredKeyPrefix() + sessionId;
}
private String getSessionCreatedChannel(String sessionId) {
return getSessionCreatedChannelPrefix() + sessionId;
}
private String getExpiredKeyPrefix() {
return this.namespace + "sessions:expires:";
}
/**
* Gets the prefix for the channel that {@link SessionCreatedEvent}s are published to.
* The suffix is the session id of the session that was created.
* @return the prefix for the channel that {@link SessionCreatedEvent}s are published
* to
*/
public String getSessionCreatedChannelPrefix() {
return this.sessionCreatedChannelPrefix;
}
/**
* Gets the name of the channel that {@link SessionDeletedEvent}s are published to.
* @return the name for the channel that {@link SessionDeletedEvent}s are published to
*/
public String getSessionDeletedChannel() {
return this.sessionDeletedChannel;
}
/**
* Gets the name of the channel that {@link SessionExpiredEvent}s are published to.
* @return the name for the channel that {@link SessionExpiredEvent}s are published to
*/
public String getSessionExpiredChannel() {
return this.sessionExpiredChannel;
}
/**
* Gets the {@link BoundHashOperations} to operate on a {@link Session}.
* @param sessionId the id of the {@link Session} to work with
* @return the {@link BoundHashOperations} to operate on a {@link Session}
*/
private BoundHashOperations<Object, Object, Object> getSessionBoundHashOperations(String sessionId) {
String key = getSessionKey(sessionId);
return this.sessionRedisOperations.boundHashOps(key);
}
/**
* Gets the key for the specified session attribute.
* @param attributeName the attribute name
* @return the attribute key name
*/
static String getSessionAttrNameKey(String attributeName) {
return RedisSessionMapper.ATTRIBUTE_PREFIX + attributeName;
}
/**
* A custom implementation of {@link Session} that uses a {@link MapSession} as the
* basis for its mapping. It keeps track of any attributes that have changed. When
* {@link org.springframework.session.data.redis.RedisOperationsSessionRepository.RedisSession#saveDelta()}
* is invoked all the attributes that have been changed will be persisted.
*
* @author Rob Winch
* @since 1.0
*/
final class RedisSession implements Session {
private final MapSession cached;
private Instant originalLastAccessTime;
private Map<String, Object> delta = new HashMap<>();
private boolean isNew;
private String originalPrincipalName;
private String originalSessionId;
RedisSession(MapSession cached, boolean isNew) {
this.cached = cached;
this.isNew = isNew;
this.originalSessionId = cached.getId();
Map<String, String> indexes = RedisOperationsSessionRepository.this.indexResolver.resolveIndexesFor(this);
this.originalPrincipalName = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
if (this.isNew) {
this.delta.put(RedisSessionMapper.CREATION_TIME_KEY, cached.getCreationTime().toEpochMilli());
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY,
(int) cached.getMaxInactiveInterval().getSeconds());
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, cached.getLastAccessedTime().toEpochMilli());
}
if (this.isNew || (RedisOperationsSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
getAttributeNames().forEach((attributeName) -> this.delta.put(getSessionAttrNameKey(attributeName),
cached.getAttribute(attributeName)));
}
}
public void setNew(boolean isNew) {
this.isNew = isNew;
}
@Override
public void setLastAccessedTime(Instant lastAccessedTime) {
this.cached.setLastAccessedTime(lastAccessedTime);
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, getLastAccessedTime().toEpochMilli());
flushImmediateIfNecessary();
}
@Override
public boolean isExpired() {
return this.cached.isExpired();
}
public boolean isNew() {
return this.isNew;
}
@Override
public Instant getCreationTime() {
return this.cached.getCreationTime();
}
@Override
public String getId() {
return this.cached.getId();
}
@Override
public String changeSessionId() {
return this.cached.changeSessionId();
}
@Override
public Instant getLastAccessedTime() {
return this.cached.getLastAccessedTime();
}
@Override
public void setMaxInactiveInterval(Duration interval) {
this.cached.setMaxInactiveInterval(interval);
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, (int) getMaxInactiveInterval().getSeconds());
flushImmediateIfNecessary();
}
@Override
public Duration getMaxInactiveInterval() {
return this.cached.getMaxInactiveInterval();
}
@Override
public <T> T getAttribute(String attributeName) {
T attributeValue = this.cached.getAttribute(attributeName);
if (attributeValue != null
&& RedisOperationsSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
this.delta.put(getSessionAttrNameKey(attributeName), attributeValue);
}
return attributeValue;
}
@Override
public Set<String> getAttributeNames() {
return this.cached.getAttributeNames();
}
@Override
public void setAttribute(String attributeName, Object attributeValue) {
this.cached.setAttribute(attributeName, attributeValue);
this.delta.put(getSessionAttrNameKey(attributeName), attributeValue);
flushImmediateIfNecessary();
}
@Override
public void removeAttribute(String attributeName) {
this.cached.removeAttribute(attributeName);
this.delta.put(getSessionAttrNameKey(attributeName), null);
flushImmediateIfNecessary();
}
private void flushImmediateIfNecessary() {
if (RedisOperationsSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {
save();
}
}
private void save() {
saveChangeSessionId();
saveDelta();
}
/**
* Saves any attributes that have been changed and updates the expiration of this
* session.
*/
private void saveDelta() {
if (this.delta.isEmpty()) {
return;
}
String sessionId = getId();
getSessionBoundHashOperations(sessionId).putAll(this.delta);
String principalSessionKey = getSessionAttrNameKey(
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
String securityPrincipalSessionKey = getSessionAttrNameKey(SPRING_SECURITY_CONTEXT);
if (this.delta.containsKey(principalSessionKey) || this.delta.containsKey(securityPrincipalSessionKey)) {
if (this.originalPrincipalName != null) {
String originalPrincipalRedisKey = getPrincipalKey(this.originalPrincipalName);
RedisOperationsSessionRepository.this.sessionRedisOperations.boundSetOps(originalPrincipalRedisKey)
.remove(sessionId);
}
Map<String, String> indexes = RedisOperationsSessionRepository.this.indexResolver
.resolveIndexesFor(this);
String principal = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
this.originalPrincipalName = principal;
if (principal != null) {
String principalRedisKey = getPrincipalKey(principal);
RedisOperationsSessionRepository.this.sessionRedisOperations.boundSetOps(principalRedisKey)
.add(sessionId);
}
}
this.delta = new HashMap<>(this.delta.size());
Long originalExpiration = (this.originalLastAccessTime != null)
? this.originalLastAccessTime.plus(getMaxInactiveInterval()).toEpochMilli() : null;
RedisOperationsSessionRepository.this.expirationPolicy.onExpirationUpdated(originalExpiration, this);
}
private void saveChangeSessionId() {
String sessionId = getId();
if (sessionId.equals(this.originalSessionId)) {
return;
}
if (!isNew()) {
String originalSessionIdKey = getSessionKey(this.originalSessionId);
String sessionIdKey = getSessionKey(sessionId);
try {
RedisOperationsSessionRepository.this.sessionRedisOperations.rename(originalSessionIdKey,
sessionIdKey);
}
catch (NonTransientDataAccessException ex) {
handleErrNoSuchKeyError(ex);
}
String originalExpiredKey = getExpiredKey(this.originalSessionId);
String expiredKey = getExpiredKey(sessionId);
try {
RedisOperationsSessionRepository.this.sessionRedisOperations.rename(originalExpiredKey, expiredKey);
}
catch (NonTransientDataAccessException ex) {
handleErrNoSuchKeyError(ex);
}
}
this.originalSessionId = sessionId;
}
private void handleErrNoSuchKeyError(NonTransientDataAccessException ex) {
if (!"ERR no such key".equals(NestedExceptionUtils.getMostSpecificCause(ex).getMessage())) {
throw ex;
}
}
}
}

View File

@@ -28,7 +28,7 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.session.Session;
import org.springframework.session.data.redis.RedisOperationsSessionRepository.RedisSession;
import org.springframework.session.data.redis.RedisIndexedSessionRepository.RedisSession;
/**
* A strategy for expiring {@link RedisSession} instances. This performs two operations:
@@ -64,13 +64,13 @@ final class RedisSessionExpirationPolicy {
this.lookupSessionKey = lookupSessionKey;
}
public void onDelete(Session session) {
void onDelete(Session session) {
long toExpire = roundUpToNextMinute(expiresInMillis(session));
String expireKey = getExpirationKey(toExpire);
this.redis.boundSetOps(expireKey).remove(session.getId());
}
public void onExpirationUpdated(Long originalExpirationTimeInMilli, Session session) {
void onExpirationUpdated(Long originalExpirationTimeInMilli, Session session) {
String keyToExpire = "expires:" + session.getId();
long toExpire = roundUpToNextMinute(expiresInMillis(session));
@@ -117,7 +117,7 @@ final class RedisSessionExpirationPolicy {
return this.lookupSessionKey.apply(sessionId);
}
public void cleanExpiredSessions() {
void cleanExpiredSessions() {
long now = System.currentTimeMillis();
long prevMin = roundDownMinute(now);

View File

@@ -40,8 +40,7 @@ import org.springframework.util.Assert;
* @author Vedran Pavic
* @since 2.2.0
*/
public class SimpleRedisOperationsSessionRepository
implements SessionRepository<SimpleRedisOperationsSessionRepository.RedisSession> {
public class RedisSessionRepository implements SessionRepository<RedisSessionRepository.RedisSession> {
private static final String DEFAULT_KEY_NAMESPACE = "spring:session:";
@@ -56,11 +55,11 @@ public class SimpleRedisOperationsSessionRepository
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
/**
* Create a new {@link SimpleRedisOperationsSessionRepository} instance.
* Create a new {@link RedisSessionRepository} instance.
* @param sessionRedisOperations the {@link RedisOperations} to use for managing
* sessions
*/
public SimpleRedisOperationsSessionRepository(RedisOperations<String, Object> sessionRedisOperations) {
public RedisSessionRepository(RedisOperations<String, Object> sessionRedisOperations) {
Assert.notNull(sessionRedisOperations, "sessionRedisOperations mut not be null");
this.sessionRedisOperations = sessionRedisOperations;
}
@@ -182,7 +181,7 @@ public class SimpleRedisOperationsSessionRepository
(int) cached.getMaxInactiveInterval().getSeconds());
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, cached.getLastAccessedTime().toEpochMilli());
}
if (this.isNew || (SimpleRedisOperationsSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
if (this.isNew || (RedisSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
getAttributeNames().forEach((attributeName) -> this.delta.put(getAttributeKey(attributeName),
cached.getAttribute(attributeName)));
}
@@ -201,8 +200,7 @@ public class SimpleRedisOperationsSessionRepository
@Override
public <T> T getAttribute(String attributeName) {
T attributeValue = this.cached.getAttribute(attributeName);
if (attributeValue != null
&& SimpleRedisOperationsSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
if (attributeValue != null && RedisSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
this.delta.put(getAttributeKey(attributeName), attributeValue);
}
return attributeValue;
@@ -260,7 +258,7 @@ public class SimpleRedisOperationsSessionRepository
}
private void flushIfRequired() {
if (SimpleRedisOperationsSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {
if (RedisSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {
save();
}
}
@@ -282,8 +280,7 @@ public class SimpleRedisOperationsSessionRepository
if (!this.isNew) {
String originalSessionIdKey = getSessionKey(this.originalSessionId);
String sessionIdKey = getSessionKey(getId());
SimpleRedisOperationsSessionRepository.this.sessionRedisOperations.rename(originalSessionIdKey,
sessionIdKey);
RedisSessionRepository.this.sessionRedisOperations.rename(originalSessionIdKey, sessionIdKey);
}
this.originalSessionId = getId();
}
@@ -294,9 +291,8 @@ public class SimpleRedisOperationsSessionRepository
return;
}
String key = getSessionKey(getId());
SimpleRedisOperationsSessionRepository.this.sessionRedisOperations.opsForHash().putAll(key,
new HashMap<>(this.delta));
SimpleRedisOperationsSessionRepository.this.sessionRedisOperations.expireAt(key,
RedisSessionRepository.this.sessionRedisOperations.opsForHash().putAll(key, new HashMap<>(this.delta));
RedisSessionRepository.this.sessionRedisOperations.expireAt(key,
Date.from(Instant.ofEpochMilli(getLastAccessedTime().toEpochMilli())
.plusSeconds(getMaxInactiveInterval().getSeconds())));
this.delta.clear();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2018 the original author or authors.
* Copyright 2014-2019 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.
@@ -81,7 +81,7 @@ public class ConfigureNotifyKeyspaceEventsAction implements ConfigureRedisAction
}
catch (InvalidDataAccessApiUsageException ex) {
throw new IllegalStateException(
"Unable to configure Redis to keyspace notifications. See https://docs.spring.io/spring-session/docs/current/reference/html5/#api-redisoperationssessionrepository-sessiondestroyedevent",
"Unable to configure Redis to keyspace notifications. See https://docs.spring.io/spring-session/docs/current/reference/html5/#api-redisindexedsessionrepository-sessiondestroyedevent",
ex);
}
}

View File

@@ -24,11 +24,11 @@ import java.lang.annotation.Target;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.session.data.redis.RedisOperationsSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
/**
* Qualifier annotation for a {@link RedisConnectionFactory} to be injected in
* {@link RedisOperationsSessionRepository}.
* {@link RedisIndexedSessionRepository}.
*
* @author Vedran Pavic
* @since 2.0.0

View File

@@ -23,14 +23,18 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.RedisSessionRepository;
/**
* Annotation used to inject the Redis accessor used by Spring Session's Redis session
* repository.
*
* @author Vedran Pavic
* @see org.springframework.session.data.redis.RedisOperationsSessionRepository#getSessionRedisOperations()
* @see org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepository#getSessionRedisOperations()
* @see RedisIndexedSessionRepository#getSessionRedisOperations()
* @see RedisSessionRepository#getSessionRedisOperations()
* @see ReactiveRedisSessionRepository#getSessionRedisOperations()
* @since 2.0.0
*/
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })

View File

@@ -32,7 +32,7 @@ import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
import org.springframework.session.data.redis.RedisFlushMode;
import org.springframework.session.data.redis.RedisOperationsSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
/**
@@ -65,7 +65,7 @@ import org.springframework.session.web.http.SessionRepositoryFilter;
@Target(ElementType.TYPE)
@Documented
@Import(RedisHttpSessionConfiguration.class)
@Configuration
@Configuration(proxyBeanMethods = false)
public @interface EnableRedisHttpSession {
/**
@@ -85,7 +85,7 @@ public @interface EnableRedisHttpSession {
* the applications and they could function within the same Redis instance.
* @return the unique namespace for keys
*/
String redisNamespace() default RedisOperationsSessionRepository.DEFAULT_NAMESPACE;
String redisNamespace() default RedisIndexedSessionRepository.DEFAULT_NAMESPACE;
/**
* Flush mode for the Redis sessions. The default is {@code ON_SAVE} which only

View File

@@ -18,8 +18,10 @@ package org.springframework.session.data.redis.config.annotation.web.http;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
import org.apache.commons.logging.LogFactory;
@@ -49,11 +51,14 @@ import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.session.FlushMode;
import org.springframework.session.IndexResolver;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.config.SessionRepositoryCustomizer;
import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration;
import org.springframework.session.data.redis.RedisFlushMode;
import org.springframework.session.data.redis.RedisOperationsSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.config.ConfigureNotifyKeyspaceEventsAction;
import org.springframework.session.data.redis.config.ConfigureRedisAction;
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory;
@@ -75,15 +80,14 @@ import org.springframework.util.StringValueResolver;
* @since 1.0
*/
@Configuration(proxyBeanMethods = false)
@EnableScheduling
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware, SchedulingConfigurer {
implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware {
static final String DEFAULT_CLEANUP_CRON = "0 * * * * *";
private Integer maxInactiveIntervalInSeconds = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
private String redisNamespace = RedisOperationsSessionRepository.DEFAULT_NAMESPACE;
private String redisNamespace = RedisIndexedSessionRepository.DEFAULT_NAMESPACE;
private FlushMode flushMode = FlushMode.ON_SAVE;
@@ -95,6 +99,8 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
private RedisConnectionFactory redisConnectionFactory;
private IndexResolver<Session> indexResolver;
private RedisSerializer<Object> defaultRedisSerializer;
private ApplicationEventPublisher applicationEventPublisher;
@@ -103,15 +109,20 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
private Executor redisSubscriptionExecutor;
private List<SessionRepositoryCustomizer<RedisIndexedSessionRepository>> sessionRepositoryCustomizers;
private ClassLoader classLoader;
private StringValueResolver embeddedValueResolver;
@Bean
public RedisOperationsSessionRepository sessionRepository() {
public RedisIndexedSessionRepository sessionRepository() {
RedisTemplate<Object, Object> redisTemplate = createRedisTemplate();
RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(redisTemplate);
RedisIndexedSessionRepository sessionRepository = new RedisIndexedSessionRepository(redisTemplate);
sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
if (this.indexResolver != null) {
sessionRepository.setIndexResolver(this.indexResolver);
}
if (this.defaultRedisSerializer != null) {
sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);
}
@@ -123,12 +134,14 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
sessionRepository.setSaveMode(this.saveMode);
int database = resolveDatabase();
sessionRepository.setDatabase(database);
this.sessionRepositoryCustomizers
.forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository));
return sessionRepository;
}
@Bean
public RedisMessageListenerContainer springSessionRedisMessageListenerContainer(
RedisOperationsSessionRepository sessionRepository) {
RedisIndexedSessionRepository sessionRepository) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(this.redisConnectionFactory);
if (this.redisTaskExecutor != null) {
@@ -209,6 +222,11 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
this.applicationEventPublisher = applicationEventPublisher;
}
@Autowired(required = false)
public void setIndexResolver(IndexResolver<Session> indexResolver) {
this.indexResolver = indexResolver;
}
@Autowired(required = false)
@Qualifier("springSessionRedisTaskExecutor")
public void setRedisTaskExecutor(Executor redisTaskExecutor) {
@@ -221,6 +239,12 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
this.redisSubscriptionExecutor = redisSubscriptionExecutor;
}
@Autowired(required = false)
public void setSessionRepositoryCustomizer(
ObjectProvider<SessionRepositoryCustomizer<RedisIndexedSessionRepository>> sessionRepositoryCustomizers) {
this.sessionRepositoryCustomizers = sessionRepositoryCustomizers.orderedStream().collect(Collectors.toList());
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
@@ -255,11 +279,6 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
}
}
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.addCronTask(() -> sessionRepository().cleanupExpiredSessions(), this.cleanupCron);
}
private RedisTemplate<Object, Object> createRedisTemplate() {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
@@ -282,7 +301,7 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
&& this.redisConnectionFactory instanceof JedisConnectionFactory) {
return ((JedisConnectionFactory) this.redisConnectionFactory).getDatabase();
}
return RedisOperationsSessionRepository.DEFAULT_DATABASE;
return RedisIndexedSessionRepository.DEFAULT_DATABASE;
}
/**
@@ -325,4 +344,25 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
}
/**
* Configuration of scheduled job for cleaning up expired sessions.
*/
@EnableScheduling
@Configuration(proxyBeanMethods = false)
class SessionCleanupConfiguration implements SchedulingConfigurer {
private final RedisIndexedSessionRepository sessionRepository;
SessionCleanupConfiguration(RedisIndexedSessionRepository sessionRepository) {
this.sessionRepository = sessionRepository;
}
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.addCronTask(this.sessionRepository::cleanupExpiredSessions,
RedisHttpSessionConfiguration.this.cleanupCron);
}
}
}

View File

@@ -30,7 +30,7 @@ import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.config.annotation.web.server.EnableSpringWebSession;
import org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepository;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
import org.springframework.session.data.redis.RedisFlushMode;
import org.springframework.web.server.session.WebSessionManager;
@@ -63,7 +63,7 @@ import org.springframework.web.server.session.WebSessionManager;
@Target(ElementType.TYPE)
@Documented
@Import(RedisWebSessionConfiguration.class)
@Configuration
@Configuration(proxyBeanMethods = false)
public @interface EnableRedisWebSession {
/**
@@ -83,7 +83,7 @@ public @interface EnableRedisWebSession {
* the applications and they could function within the same Redis instance.
* @return the unique namespace for keys
*/
String redisNamespace() default ReactiveRedisOperationsSessionRepository.DEFAULT_NAMESPACE;
String redisNamespace() default ReactiveRedisSessionRepository.DEFAULT_NAMESPACE;
/**
* Flush mode for the Redis sessions. The default is {@code ON_SAVE} which only

View File

@@ -16,7 +16,9 @@
package org.springframework.session.data.redis.config.annotation.web.server;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.ObjectProvider;
@@ -36,8 +38,9 @@ import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.config.ReactiveSessionRepositoryCustomizer;
import org.springframework.session.config.annotation.web.server.SpringWebSessionConfiguration;
import org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepository;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
import org.springframework.session.data.redis.RedisFlushMode;
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory;
import org.springframework.util.Assert;
@@ -60,7 +63,7 @@ public class RedisWebSessionConfiguration extends SpringWebSessionConfiguration
private Integer maxInactiveIntervalInSeconds = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
private String redisNamespace = ReactiveRedisOperationsSessionRepository.DEFAULT_NAMESPACE;
private String redisNamespace = ReactiveRedisSessionRepository.DEFAULT_NAMESPACE;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
@@ -68,20 +71,23 @@ public class RedisWebSessionConfiguration extends SpringWebSessionConfiguration
private RedisSerializer<Object> defaultRedisSerializer;
private List<ReactiveSessionRepositoryCustomizer<ReactiveRedisSessionRepository>> sessionRepositoryCustomizers;
private ClassLoader classLoader;
private StringValueResolver embeddedValueResolver;
@Bean
public ReactiveRedisOperationsSessionRepository sessionRepository() {
public ReactiveRedisSessionRepository sessionRepository() {
ReactiveRedisTemplate<String, Object> reactiveRedisTemplate = createReactiveRedisTemplate();
ReactiveRedisOperationsSessionRepository sessionRepository = new ReactiveRedisOperationsSessionRepository(
reactiveRedisTemplate);
ReactiveRedisSessionRepository sessionRepository = new ReactiveRedisSessionRepository(reactiveRedisTemplate);
sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
if (StringUtils.hasText(this.redisNamespace)) {
sessionRepository.setRedisKeyNamespace(this.redisNamespace);
}
sessionRepository.setSaveMode(this.saveMode);
this.sessionRepositoryCustomizers
.forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository));
return sessionRepository;
}
@@ -120,6 +126,12 @@ public class RedisWebSessionConfiguration extends SpringWebSessionConfiguration
this.defaultRedisSerializer = defaultRedisSerializer;
}
@Autowired(required = false)
public void setSessionRepositoryCustomizer(
ObjectProvider<ReactiveSessionRepositoryCustomizer<ReactiveRedisSessionRepository>> sessionRepositoryCustomizers) {
this.sessionRepositoryCustomizers = sessionRepositoryCustomizers.orderedStream().collect(Collectors.toList());
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;

View File

@@ -33,7 +33,7 @@ import org.springframework.data.redis.core.ReactiveHashOperations;
import org.springframework.data.redis.core.ReactiveRedisOperations;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepository.RedisSession;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository.RedisSession;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
@@ -46,11 +46,11 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
/**
* Tests for {@link ReactiveRedisOperationsSessionRepository}.
* Tests for {@link ReactiveRedisSessionRepository}.
*
* @author Vedran Pavic
*/
class ReactiveRedisOperationsSessionRepositoryTests {
class ReactiveRedisSessionRepositoryTests {
@SuppressWarnings("unchecked")
private ReactiveRedisOperations<String, Object> redisOperations = mock(ReactiveRedisOperations.class);
@@ -61,13 +61,13 @@ class ReactiveRedisOperationsSessionRepositoryTests {
@SuppressWarnings("unchecked")
private ArgumentCaptor<Map<String, Object>> delta = ArgumentCaptor.forClass(Map.class);
private ReactiveRedisOperationsSessionRepository repository;
private ReactiveRedisSessionRepository repository;
private MapSession cached;
@BeforeEach
void setUp() {
this.repository = new ReactiveRedisOperationsSessionRepository(this.redisOperations);
this.repository = new ReactiveRedisSessionRepository(this.redisOperations);
this.cached = new MapSession();
this.cached.setId("session-id");
@@ -77,7 +77,7 @@ class ReactiveRedisOperationsSessionRepositoryTests {
@Test
void constructorWithNullReactiveRedisOperations() {
assertThatIllegalArgumentException().isThrownBy(() -> new ReactiveRedisOperationsSessionRepository(null))
assertThatIllegalArgumentException().isThrownBy(() -> new ReactiveRedisSessionRepository(null))
.withMessageContaining("sessionRedisOperations cannot be null");
}
@@ -206,7 +206,7 @@ class ReactiveRedisOperationsSessionRepositoryTests {
verifyZeroInteractions(this.hashOperations);
assertThat(this.delta.getAllValues().get(0)).isEqualTo(
map(RedisOperationsSessionRepository.getSessionAttrNameKey(attrName), session.getAttribute(attrName)));
map(RedisIndexedSessionRepository.getSessionAttrNameKey(attrName), session.getAttribute(attrName)));
}
@Test
@@ -229,7 +229,7 @@ class ReactiveRedisOperationsSessionRepositoryTests {
verifyZeroInteractions(this.hashOperations);
assertThat(this.delta.getAllValues().get(0))
.isEqualTo(map(RedisOperationsSessionRepository.getSessionAttrNameKey(attrName), null));
.isEqualTo(map(RedisIndexedSessionRepository.getSessionAttrNameKey(attrName), null));
}
@Test

View File

@@ -37,8 +37,6 @@ import org.mockito.MockitoAnnotations;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.redis.connection.DefaultMessage;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.BoundValueOperations;
@@ -50,7 +48,7 @@ import org.springframework.session.FlushMode;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.data.redis.RedisOperationsSessionRepository.RedisSession;
import org.springframework.session.data.redis.RedisIndexedSessionRepository.RedisSession;
import org.springframework.session.events.AbstractSessionEvent;
import static org.assertj.core.api.Assertions.assertThat;
@@ -67,49 +65,40 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
@SuppressWarnings({ "unchecked", "rawtypes", "deprecation" })
class RedisOperationsSessionRepositoryTests {
private static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT";
class RedisIndexedSessionRepositoryTests {
@Mock
RedisConnectionFactory factory;
private RedisOperations<Object, Object> redisOperations;
@Mock
RedisConnection connection;
private BoundValueOperations<Object, Object> boundValueOperations;
@Mock
RedisOperations<Object, Object> redisOperations;
private BoundHashOperations<Object, Object, Object> boundHashOperations;
@Mock
BoundValueOperations<Object, Object> boundValueOperations;
private BoundSetOperations<Object, Object> boundSetOperations;
@Mock
BoundHashOperations<Object, Object, Object> boundHashOperations;
private ApplicationEventPublisher publisher;
@Mock
BoundSetOperations<Object, Object> boundSetOperations;
@Mock
ApplicationEventPublisher publisher;
@Mock
RedisSerializer<Object> defaultSerializer;
private RedisSerializer<Object> defaultSerializer;
@Captor
ArgumentCaptor<AbstractSessionEvent> event;
private ArgumentCaptor<AbstractSessionEvent> event;
@Captor
ArgumentCaptor<Map<String, Object>> delta;
private ArgumentCaptor<Map<String, Object>> delta;
private MapSession cached;
private RedisOperationsSessionRepository redisRepository;
private RedisIndexedSessionRepository redisRepository;
@BeforeEach
void setup() {
MockitoAnnotations.initMocks(this);
this.redisRepository = new RedisOperationsSessionRepository(this.redisOperations);
this.redisRepository = new RedisIndexedSessionRepository(this.redisOperations);
this.redisRepository.setDefaultSerializer(this.defaultSerializer);
this.cached = new MapSession();
@@ -277,7 +266,7 @@ class RedisOperationsSessionRepositoryTests {
this.redisRepository.save(session);
assertThat(getDelta()).isEqualTo(
map(RedisOperationsSessionRepository.getSessionAttrNameKey(attrName), session.getAttribute(attrName)));
map(RedisIndexedSessionRepository.getSessionAttrNameKey(attrName), session.getAttribute(attrName)));
}
@Test
@@ -291,7 +280,7 @@ class RedisOperationsSessionRepositoryTests {
this.redisRepository.save(session);
assertThat(getDelta()).isEqualTo(map(RedisOperationsSessionRepository.getSessionAttrNameKey(attrName), null));
assertThat(getDelta()).isEqualTo(map(RedisIndexedSessionRepository.getSessionAttrNameKey(attrName), null));
}
@Test
@@ -320,6 +309,7 @@ class RedisOperationsSessionRepositoryTests {
}
@Test
@SuppressWarnings("unchecked")
void delete() {
String attrName = "attrName";
MapSession expected = new MapSession();
@@ -327,7 +317,7 @@ class RedisOperationsSessionRepositoryTests {
expected.setAttribute(attrName, "attrValue");
given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations);
given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations);
Map map = map(RedisOperationsSessionRepository.getSessionAttrNameKey(attrName), expected.getAttribute(attrName),
Map map = map(RedisIndexedSessionRepository.getSessionAttrNameKey(attrName), expected.getAttribute(attrName),
RedisSessionMapper.CREATION_TIME_KEY, expected.getCreationTime().toEpochMilli(),
RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, (int) expected.getMaxInactiveInterval().getSeconds(),
RedisSessionMapper.LAST_ACCESSED_TIME_KEY, expected.getLastAccessedTime().toEpochMilli());
@@ -353,6 +343,7 @@ class RedisOperationsSessionRepositoryTests {
}
@Test
@SuppressWarnings("unchecked")
void getSessionNotFound() {
String id = "abc";
given(this.redisOperations.boundHashOps(getKey(id))).willReturn(this.boundHashOperations);
@@ -362,6 +353,7 @@ class RedisOperationsSessionRepositoryTests {
}
@Test
@SuppressWarnings("unchecked")
void getSessionFound() {
String attribute1 = "attribute1";
String attribute2 = "attribute2";
@@ -370,8 +362,8 @@ class RedisOperationsSessionRepositoryTests {
expected.setAttribute(attribute1, "test");
expected.setAttribute(attribute2, null);
given(this.redisOperations.boundHashOps(getKey(expected.getId()))).willReturn(this.boundHashOperations);
Map map = map(RedisOperationsSessionRepository.getSessionAttrNameKey(attribute1),
expected.getAttribute(attribute1), RedisOperationsSessionRepository.getSessionAttrNameKey(attribute2),
Map map = map(RedisIndexedSessionRepository.getSessionAttrNameKey(attribute1),
expected.getAttribute(attribute1), RedisIndexedSessionRepository.getSessionAttrNameKey(attribute2),
expected.getAttribute(attribute2), RedisSessionMapper.CREATION_TIME_KEY,
expected.getCreationTime().toEpochMilli(), RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY,
(int) expected.getMaxInactiveInterval().getSeconds(), RedisSessionMapper.LAST_ACCESSED_TIME_KEY,
@@ -391,6 +383,7 @@ class RedisOperationsSessionRepositoryTests {
}
@Test
@SuppressWarnings("unchecked")
void getSessionExpired() {
String expiredId = "expired-id";
given(this.redisOperations.boundHashOps(getKey(expiredId))).willReturn(this.boundHashOperations);
@@ -402,6 +395,7 @@ class RedisOperationsSessionRepositoryTests {
}
@Test
@SuppressWarnings("unchecked")
void findByPrincipalNameExpired() {
String expiredId = "expired-id";
given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations);
@@ -417,6 +411,7 @@ class RedisOperationsSessionRepositoryTests {
}
@Test
@SuppressWarnings("unchecked")
void findByPrincipalName() {
Instant lastAccessed = Instant.now().minusMillis(10);
Instant createdTime = lastAccessed.minusMillis(10);
@@ -446,7 +441,6 @@ class RedisOperationsSessionRepositoryTests {
@Test
void cleanupExpiredSessions() {
String expiredId = "expired-id";
given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations);
Set<Object> expiredIds = new HashSet<>(Arrays.asList("expired-key1", "expired-key2"));
@@ -497,6 +491,7 @@ class RedisOperationsSessionRepositoryTests {
}
@Test
@SuppressWarnings("unchecked")
void onMessageDeletedSessionFound() {
String deletedId = "deleted-id";
given(this.redisOperations.boundHashOps(getKey(deletedId))).willReturn(this.boundHashOperations);
@@ -523,6 +518,7 @@ class RedisOperationsSessionRepositoryTests {
}
@Test
@SuppressWarnings("unchecked")
void onMessageDeletedSessionNotFound() {
String deletedId = "deleted-id";
given(this.redisOperations.boundHashOps(getKey(deletedId))).willReturn(this.boundHashOperations);
@@ -545,6 +541,7 @@ class RedisOperationsSessionRepositoryTests {
}
@Test
@SuppressWarnings("unchecked")
void onMessageExpiredSessionFound() {
String expiredId = "expired-id";
given(this.redisOperations.boundHashOps(getKey(expiredId))).willReturn(this.boundHashOperations);
@@ -571,6 +568,7 @@ class RedisOperationsSessionRepositoryTests {
}
@Test
@SuppressWarnings("unchecked")
void onMessageExpiredSessionNotFound() {
String expiredId = "expired-id";
given(this.redisOperations.boundHashOps(getKey(expiredId))).willReturn(this.boundHashOperations);
@@ -677,7 +675,7 @@ class RedisOperationsSessionRepositoryTests {
Map<String, Object> delta = getDelta(2);
assertThat(delta.size()).isEqualTo(1);
assertThat(delta).isEqualTo(
map(RedisOperationsSessionRepository.getSessionAttrNameKey(attrName), session.getAttribute(attrName)));
map(RedisIndexedSessionRepository.getSessionAttrNameKey(attrName), session.getAttribute(attrName)));
}
@Test
@@ -694,10 +692,11 @@ class RedisOperationsSessionRepositoryTests {
Map<String, Object> delta = getDelta(2);
assertThat(delta.size()).isEqualTo(1);
assertThat(delta).isEqualTo(
map(RedisOperationsSessionRepository.getSessionAttrNameKey(attrName), session.getAttribute(attrName)));
map(RedisIndexedSessionRepository.getSessionAttrNameKey(attrName), session.getAttribute(attrName)));
}
@Test
@SuppressWarnings("unchecked")
void flushModeSetMaxInactiveIntervalInSeconds() {
given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations);
given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations);
@@ -736,12 +735,6 @@ class RedisOperationsSessionRepositoryTests {
.withMessage("flushMode cannot be null");
}
@Test
void setRedisFlushModeNull() {
assertThatIllegalArgumentException().isThrownBy(() -> this.redisRepository.setRedisFlushMode(null))
.withMessage("redisFlushMode cannot be null");
}
@Test
void changeRedisNamespace() {
String namespace = "foo:bar";

View File

@@ -66,7 +66,7 @@ class RedisSessionExpirationPolicyTests {
@BeforeEach
void setup() {
MockitoAnnotations.initMocks(this);
RedisOperationsSessionRepository repository = new RedisOperationsSessionRepository(this.sessionRedisOperations);
RedisIndexedSessionRepository repository = new RedisIndexedSessionRepository(this.sessionRedisOperations);
this.policy = new RedisSessionExpirationPolicy(this.sessionRedisOperations, repository::getExpirationsKey,
repository::getSessionKey);
this.session = new MapSession();

View File

@@ -36,7 +36,7 @@ import org.springframework.data.redis.core.RedisOperations;
import org.springframework.session.FlushMode;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.data.redis.SimpleRedisOperationsSessionRepository.RedisSession;
import org.springframework.session.data.redis.RedisSessionRepository.RedisSession;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
@@ -48,11 +48,11 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
/**
* Tests for {@link SimpleRedisOperationsSessionRepository}.
* Tests for {@link RedisSessionRepository}.
*
* @author Vedran Pavic
*/
class SimpleRedisOperationsSessionRepositoryTests {
class RedisSessionRepositoryTests {
private static final String TEST_SESSION_ID = "session-id";
@@ -67,18 +67,18 @@ class SimpleRedisOperationsSessionRepositoryTests {
@Captor
private ArgumentCaptor<Map<String, Object>> delta;
private SimpleRedisOperationsSessionRepository sessionRepository;
private RedisSessionRepository sessionRepository;
@BeforeEach
void setUp() {
MockitoAnnotations.initMocks(this);
given(this.sessionRedisOperations.<String, Object>opsForHash()).willReturn(this.sessionHashOperations);
this.sessionRepository = new SimpleRedisOperationsSessionRepository(this.sessionRedisOperations);
this.sessionRepository = new RedisSessionRepository(this.sessionRedisOperations);
}
@Test
void constructor_NullRedisOperations_ShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> new ReactiveRedisOperationsSessionRepository(null))
assertThatIllegalArgumentException().isThrownBy(() -> new ReactiveRedisSessionRepository(null))
.withMessageContaining("sessionRedisOperations cannot be null");
}

View File

@@ -46,12 +46,12 @@ class RedisHttpSessionConfigurationNoOpConfigureRedisActionTests {
static class Config {
@Bean
public static ConfigureRedisAction configureRedisAction() {
ConfigureRedisAction configureRedisAction() {
return ConfigureRedisAction.NO_OP;
}
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisConnectionFactory redisConnectionFactory() {
return mock(RedisConnectionFactory.class);
}

View File

@@ -65,12 +65,12 @@ class RedisHttpSessionConfigurationOverrideDefaultSerializerTests {
@Bean
@SuppressWarnings("unchecked")
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return mock(RedisSerializer.class);
}
@Bean
public RedisConnectionFactory connectionFactory() {
RedisConnectionFactory connectionFactory() {
RedisConnectionFactory factory = mock(RedisConnectionFactory.class);
RedisConnection connection = mock(RedisConnection.class);
given(factory.getConnection()).willReturn(connection);

View File

@@ -65,12 +65,12 @@ class RedisHttpSessionConfigurationOverrideSessionTaskExecutor {
static class Config {
@Bean
public Executor springSessionRedisTaskExecutor() {
Executor springSessionRedisTaskExecutor() {
return mock(Executor.class);
}
@Bean
public RedisConnectionFactory connectionFactory() {
RedisConnectionFactory connectionFactory() {
RedisConnectionFactory factory = mock(RedisConnectionFactory.class);
RedisConnection connection = mock(RedisConnection.class);
given(factory.getConnection()).willReturn(connection);

View File

@@ -71,17 +71,17 @@ class RedisHttpSessionConfigurationOverrideSessionTaskExecutors {
static class Config {
@Bean
public Executor springSessionRedisTaskExecutor() {
Executor springSessionRedisTaskExecutor() {
return mock(Executor.class);
}
@Bean
public Executor springSessionRedisSubscriptionExecutor() {
Executor springSessionRedisSubscriptionExecutor() {
return mock(Executor.class);
}
@Bean
public RedisConnectionFactory connectionFactory() {
RedisConnectionFactory connectionFactory() {
RedisConnectionFactory factory = mock(RedisConnectionFactory.class);
RedisConnection connection = mock(RedisConnection.class);
given(factory.getConnection()).willReturn(connection);

View File

@@ -29,15 +29,19 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.session.FlushMode;
import org.springframework.session.IndexResolver;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.config.SessionRepositoryCustomizer;
import org.springframework.session.data.redis.RedisFlushMode;
import org.springframework.session.data.redis.RedisOperationsSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory;
import org.springframework.test.util.ReflectionTestUtils;
@@ -54,9 +58,10 @@ import static org.mockito.Mockito.mock;
* @author Mark Paluch
* @author Vedran Pavic
*/
@SuppressWarnings("deprecation")
class RedisHttpSessionConfigurationTests {
private static final int MAX_INACTIVE_INTERVAL_IN_SECONDS = 600;
private static final String CLEANUP_CRON_EXPRESSION = "0 0 * * * *";
private AnnotationConfigApplicationContext context;
@@ -93,8 +98,7 @@ class RedisHttpSessionConfigurationTests {
@Test
void customFlushImmediately() {
registerAndRefresh(RedisConfig.class, CustomFlushImmediatelyConfiguration.class);
RedisOperationsSessionRepository sessionRepository = this.context
.getBean(RedisOperationsSessionRepository.class);
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
assertThat(sessionRepository).isNotNull();
assertThat(ReflectionTestUtils.getField(sessionRepository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
}
@@ -102,8 +106,7 @@ class RedisHttpSessionConfigurationTests {
@Test
void customFlushImmediatelyLegacy() {
registerAndRefresh(RedisConfig.class, CustomFlushImmediatelyLegacyConfiguration.class);
RedisOperationsSessionRepository sessionRepository = this.context
.getBean(RedisOperationsSessionRepository.class);
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
assertThat(sessionRepository).isNotNull();
assertThat(ReflectionTestUtils.getField(sessionRepository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
}
@@ -111,8 +114,7 @@ class RedisHttpSessionConfigurationTests {
@Test
void setCustomFlushImmediately() {
registerAndRefresh(RedisConfig.class, CustomFlushImmediatelySetConfiguration.class);
RedisOperationsSessionRepository sessionRepository = this.context
.getBean(RedisOperationsSessionRepository.class);
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
assertThat(sessionRepository).isNotNull();
assertThat(ReflectionTestUtils.getField(sessionRepository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
}
@@ -120,8 +122,7 @@ class RedisHttpSessionConfigurationTests {
@Test
void setCustomFlushImmediatelyLegacy() {
registerAndRefresh(RedisConfig.class, CustomFlushImmediatelySetLegacyConfiguration.class);
RedisOperationsSessionRepository sessionRepository = this.context
.getBean(RedisOperationsSessionRepository.class);
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
assertThat(sessionRepository).isNotNull();
assertThat(ReflectionTestUtils.getField(sessionRepository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
}
@@ -147,14 +148,14 @@ class RedisHttpSessionConfigurationTests {
@Test
void customSaveModeAnnotation() {
registerAndRefresh(RedisConfig.class, CustomSaveModeExpressionAnnotationConfiguration.class);
assertThat(this.context.getBean(RedisOperationsSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
assertThat(this.context.getBean(RedisIndexedSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
SaveMode.ALWAYS);
}
@Test
void customSaveModeSetter() {
registerAndRefresh(RedisConfig.class, CustomSaveModeExpressionSetterConfiguration.class);
assertThat(this.context.getBean(RedisOperationsSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
assertThat(this.context.getBean(RedisIndexedSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
SaveMode.ALWAYS);
}
@@ -162,7 +163,7 @@ class RedisHttpSessionConfigurationTests {
void qualifiedConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, QualifiedConnectionFactoryRedisConfig.class);
RedisOperationsSessionRepository repository = this.context.getBean(RedisOperationsSessionRepository.class);
RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class);
RedisConnectionFactory redisConnectionFactory = this.context.getBean("qualifiedRedisConnectionFactory",
RedisConnectionFactory.class);
assertThat(repository).isNotNull();
@@ -178,7 +179,7 @@ class RedisHttpSessionConfigurationTests {
void primaryConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, PrimaryConnectionFactoryRedisConfig.class);
RedisOperationsSessionRepository repository = this.context.getBean(RedisOperationsSessionRepository.class);
RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class);
RedisConnectionFactory redisConnectionFactory = this.context.getBean("primaryRedisConnectionFactory",
RedisConnectionFactory.class);
assertThat(repository).isNotNull();
@@ -194,7 +195,7 @@ class RedisHttpSessionConfigurationTests {
void qualifiedAndPrimaryConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, QualifiedAndPrimaryConnectionFactoryRedisConfig.class);
RedisOperationsSessionRepository repository = this.context.getBean(RedisOperationsSessionRepository.class);
RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class);
RedisConnectionFactory redisConnectionFactory = this.context.getBean("qualifiedRedisConnectionFactory",
RedisConnectionFactory.class);
assertThat(repository).isNotNull();
@@ -210,7 +211,7 @@ class RedisHttpSessionConfigurationTests {
void namedConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, NamedConnectionFactoryRedisConfig.class);
RedisOperationsSessionRepository repository = this.context.getBean(RedisOperationsSessionRepository.class);
RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class);
RedisConnectionFactory redisConnectionFactory = this.context.getBean("redisConnectionFactory",
RedisConnectionFactory.class);
assertThat(repository).isNotNull();
@@ -229,6 +230,17 @@ class RedisHttpSessionConfigurationTests {
.withMessageContaining("expected single matching bean but found 2");
}
@Test
void customIndexResolverConfiguration() {
registerAndRefresh(RedisConfig.class, CustomIndexResolverConfiguration.class);
RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class);
@SuppressWarnings("unchecked")
IndexResolver<Session> indexResolver = this.context.getBean(IndexResolver.class);
assertThat(repository).isNotNull();
assertThat(indexResolver).isNotNull();
assertThat(repository).hasFieldOrPropertyWithValue("indexResolver", indexResolver);
}
@Test // gh-1252
void customRedisMessageListenerContainerConfig() {
registerAndRefresh(RedisConfig.class, CustomRedisMessageListenerContainerConfig.class);
@@ -238,6 +250,14 @@ class RedisHttpSessionConfigurationTests {
assertThat(beans).containsKeys("springSessionRedisMessageListenerContainer", "redisMessageListenerContainer");
}
@Test
void sessionRepositoryCustomizer() {
registerAndRefresh(RedisConfig.class, SessionRepositoryCustomizerConfiguration.class);
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
assertThat(sessionRepository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval",
MAX_INACTIVE_INTERVAL_IN_SECONDS);
}
private void registerAndRefresh(Class<?>... annotatedClasses) {
this.context.register(annotatedClasses);
this.context.refresh();
@@ -255,7 +275,7 @@ class RedisHttpSessionConfigurationTests {
static class PropertySourceConfiguration {
@Bean
public PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
@@ -265,7 +285,7 @@ class RedisHttpSessionConfigurationTests {
static class RedisConfig {
@Bean
public RedisConnectionFactory defaultRedisConnectionFactory() {
RedisConnectionFactory defaultRedisConnectionFactory() {
return mockRedisConnectionFactory();
}
@@ -281,6 +301,7 @@ class RedisHttpSessionConfigurationTests {
}
@Configuration
@SuppressWarnings("deprecation")
static class CustomFlushImmediatelySetLegacyConfiguration extends RedisHttpSessionConfiguration {
CustomFlushImmediatelySetLegacyConfiguration() {
@@ -297,6 +318,7 @@ class RedisHttpSessionConfigurationTests {
@Configuration
@EnableRedisHttpSession(redisFlushMode = RedisFlushMode.IMMEDIATE)
@SuppressWarnings("deprecation")
static class CustomFlushImmediatelyLegacyConfiguration {
}
@@ -335,7 +357,7 @@ class RedisHttpSessionConfigurationTests {
@Bean
@SpringSessionRedisConnectionFactory
public RedisConnectionFactory qualifiedRedisConnectionFactory() {
RedisConnectionFactory qualifiedRedisConnectionFactory() {
return mockRedisConnectionFactory();
}
@@ -347,7 +369,7 @@ class RedisHttpSessionConfigurationTests {
@Bean
@Primary
public RedisConnectionFactory primaryRedisConnectionFactory() {
RedisConnectionFactory primaryRedisConnectionFactory() {
return mockRedisConnectionFactory();
}
@@ -359,13 +381,13 @@ class RedisHttpSessionConfigurationTests {
@Bean
@SpringSessionRedisConnectionFactory
public RedisConnectionFactory qualifiedRedisConnectionFactory() {
RedisConnectionFactory qualifiedRedisConnectionFactory() {
return mockRedisConnectionFactory();
}
@Bean
@Primary
public RedisConnectionFactory primaryRedisConnectionFactory() {
RedisConnectionFactory primaryRedisConnectionFactory() {
return mockRedisConnectionFactory();
}
@@ -376,7 +398,7 @@ class RedisHttpSessionConfigurationTests {
static class NamedConnectionFactoryRedisConfig {
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisConnectionFactory redisConnectionFactory() {
return mockRedisConnectionFactory();
}
@@ -387,7 +409,7 @@ class RedisHttpSessionConfigurationTests {
static class MultipleConnectionFactoryRedisConfig {
@Bean
public RedisConnectionFactory secondaryRedisConnectionFactory() {
RedisConnectionFactory secondaryRedisConnectionFactory() {
return mockRedisConnectionFactory();
}
@@ -405,15 +427,45 @@ class RedisHttpSessionConfigurationTests {
}
@Configuration
@EnableRedisHttpSession
static class CustomIndexResolverConfiguration {
@Bean
@SuppressWarnings("unchecked")
IndexResolver<Session> indexResolver() {
return mock(IndexResolver.class);
}
}
@Configuration
@EnableRedisHttpSession
static class CustomRedisMessageListenerContainerConfig {
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer() {
RedisMessageListenerContainer redisMessageListenerContainer() {
return new RedisMessageListenerContainer();
}
}
@EnableRedisHttpSession
static class SessionRepositoryCustomizerConfiguration {
@Bean
@Order(0)
SessionRepositoryCustomizer<RedisIndexedSessionRepository> sessionRepositoryCustomizerOne() {
return (sessionRepository) -> sessionRepository.setDefaultMaxInactiveInterval(0);
}
@Bean
@Order(1)
SessionRepositoryCustomizer<RedisIndexedSessionRepository> sessionRepositoryCustomizerTwo() {
return (sessionRepository) -> sessionRepository
.setDefaultMaxInactiveInterval(MAX_INACTIVE_INTERVAL_IN_SECONDS);
}
}
}

View File

@@ -27,7 +27,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.session.data.redis.RedisOperationsSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@@ -42,7 +42,6 @@ import static org.mockito.Mockito.mock;
*
* @author Rob Winch
* @author Mark Paluch
* @since 1.0.2
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@@ -63,16 +62,15 @@ class Gh109Tests {
* override sessionRepository construction to set the custom session-timeout
*/
@Bean
public RedisOperationsSessionRepository sessionRepository(RedisOperations<Object, Object> sessionRedisTemplate,
RedisIndexedSessionRepository sessionRepository(RedisOperations<Object, Object> sessionRedisTemplate,
ApplicationEventPublisher applicationEventPublisher) {
RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(
sessionRedisTemplate);
RedisIndexedSessionRepository sessionRepository = new RedisIndexedSessionRepository(sessionRedisTemplate);
sessionRepository.setDefaultMaxInactiveInterval(this.sessionTimeout);
return sessionRepository;
}
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisConnectionFactory redisConnectionFactory() {
RedisConnectionFactory factory = mock(RedisConnectionFactory.class);
RedisConnection connection = mock(RedisConnection.class);
given(factory.getConnection()).willReturn(connection);

View File

@@ -25,12 +25,14 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.core.ReactiveRedisOperations;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.SaveMode;
import org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepository;
import org.springframework.session.config.ReactiveSessionRepositoryCustomizer;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory;
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisOperations;
import org.springframework.test.util.ReflectionTestUtils;
@@ -68,8 +70,7 @@ class RedisWebSessionConfigurationTests {
void defaultConfiguration() {
registerAndRefresh(RedisConfig.class, DefaultConfig.class);
ReactiveRedisOperationsSessionRepository repository = this.context
.getBean(ReactiveRedisOperationsSessionRepository.class);
ReactiveRedisSessionRepository repository = this.context.getBean(ReactiveRedisSessionRepository.class);
assertThat(repository).isNotNull();
}
@@ -77,8 +78,7 @@ class RedisWebSessionConfigurationTests {
void springSessionRedisOperationsResolvingConfiguration() {
registerAndRefresh(RedisConfig.class, SpringSessionRedisOperationsResolvingConfig.class);
ReactiveRedisOperationsSessionRepository repository = this.context
.getBean(ReactiveRedisOperationsSessionRepository.class);
ReactiveRedisSessionRepository repository = this.context.getBean(ReactiveRedisSessionRepository.class);
assertThat(repository).isNotNull();
ReactiveRedisOperations<String, Object> springSessionRedisOperations = this.context
.getBean(SpringSessionRedisOperationsResolvingConfig.class).getSpringSessionRedisOperations();
@@ -91,8 +91,7 @@ class RedisWebSessionConfigurationTests {
void customNamespace() {
registerAndRefresh(RedisConfig.class, CustomNamespaceConfig.class);
ReactiveRedisOperationsSessionRepository repository = this.context
.getBean(ReactiveRedisOperationsSessionRepository.class);
ReactiveRedisSessionRepository repository = this.context.getBean(ReactiveRedisSessionRepository.class);
assertThat(repository).isNotNull();
assertThat(ReflectionTestUtils.getField(repository, "namespace")).isEqualTo(REDIS_NAMESPACE + ":");
}
@@ -101,8 +100,7 @@ class RedisWebSessionConfigurationTests {
void customMaxInactiveInterval() {
registerAndRefresh(RedisConfig.class, CustomMaxInactiveIntervalConfig.class);
ReactiveRedisOperationsSessionRepository repository = this.context
.getBean(ReactiveRedisOperationsSessionRepository.class);
ReactiveRedisSessionRepository repository = this.context.getBean(ReactiveRedisSessionRepository.class);
assertThat(repository).isNotNull();
assertThat(ReflectionTestUtils.getField(repository, "defaultMaxInactiveInterval"))
.isEqualTo(MAX_INACTIVE_INTERVAL_IN_SECONDS);
@@ -111,23 +109,22 @@ class RedisWebSessionConfigurationTests {
@Test
void customSaveModeAnnotation() {
registerAndRefresh(RedisConfig.class, CustomSaveModeExpressionAnnotationConfiguration.class);
assertThat(this.context.getBean(ReactiveRedisOperationsSessionRepository.class))
.hasFieldOrPropertyWithValue("saveMode", SaveMode.ALWAYS);
assertThat(this.context.getBean(ReactiveRedisSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
SaveMode.ALWAYS);
}
@Test
void customSaveModeSetter() {
registerAndRefresh(RedisConfig.class, CustomSaveModeExpressionSetterConfiguration.class);
assertThat(this.context.getBean(ReactiveRedisOperationsSessionRepository.class))
.hasFieldOrPropertyWithValue("saveMode", SaveMode.ALWAYS);
assertThat(this.context.getBean(ReactiveRedisSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
SaveMode.ALWAYS);
}
@Test
void qualifiedConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, QualifiedConnectionFactoryRedisConfig.class);
ReactiveRedisOperationsSessionRepository repository = this.context
.getBean(ReactiveRedisOperationsSessionRepository.class);
ReactiveRedisSessionRepository repository = this.context.getBean(ReactiveRedisSessionRepository.class);
ReactiveRedisConnectionFactory redisConnectionFactory = this.context.getBean("qualifiedRedisConnectionFactory",
ReactiveRedisConnectionFactory.class);
assertThat(repository).isNotNull();
@@ -143,8 +140,7 @@ class RedisWebSessionConfigurationTests {
void primaryConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, PrimaryConnectionFactoryRedisConfig.class);
ReactiveRedisOperationsSessionRepository repository = this.context
.getBean(ReactiveRedisOperationsSessionRepository.class);
ReactiveRedisSessionRepository repository = this.context.getBean(ReactiveRedisSessionRepository.class);
ReactiveRedisConnectionFactory redisConnectionFactory = this.context.getBean("primaryRedisConnectionFactory",
ReactiveRedisConnectionFactory.class);
assertThat(repository).isNotNull();
@@ -160,8 +156,7 @@ class RedisWebSessionConfigurationTests {
void qualifiedAndPrimaryConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, QualifiedAndPrimaryConnectionFactoryRedisConfig.class);
ReactiveRedisOperationsSessionRepository repository = this.context
.getBean(ReactiveRedisOperationsSessionRepository.class);
ReactiveRedisSessionRepository repository = this.context.getBean(ReactiveRedisSessionRepository.class);
ReactiveRedisConnectionFactory redisConnectionFactory = this.context.getBean("qualifiedRedisConnectionFactory",
ReactiveRedisConnectionFactory.class);
assertThat(repository).isNotNull();
@@ -177,8 +172,7 @@ class RedisWebSessionConfigurationTests {
void namedConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, NamedConnectionFactoryRedisConfig.class);
ReactiveRedisOperationsSessionRepository repository = this.context
.getBean(ReactiveRedisOperationsSessionRepository.class);
ReactiveRedisSessionRepository repository = this.context.getBean(ReactiveRedisSessionRepository.class);
ReactiveRedisConnectionFactory redisConnectionFactory = this.context.getBean("redisConnectionFactory",
ReactiveRedisConnectionFactory.class);
assertThat(repository).isNotNull();
@@ -198,12 +192,11 @@ class RedisWebSessionConfigurationTests {
}
@Test
@SuppressWarnings("unchecked")
void customRedisSerializerConfig() {
registerAndRefresh(RedisConfig.class, CustomRedisSerializerConfig.class);
ReactiveRedisOperationsSessionRepository repository = this.context
.getBean(ReactiveRedisOperationsSessionRepository.class);
ReactiveRedisSessionRepository repository = this.context.getBean(ReactiveRedisSessionRepository.class);
@SuppressWarnings("unchecked")
RedisSerializer<Object> redisSerializer = this.context.getBean("springSessionDefaultRedisSerializer",
RedisSerializer.class);
assertThat(repository).isNotNull();
@@ -222,6 +215,14 @@ class RedisWebSessionConfigurationTests {
"serializer")).isEqualTo(redisSerializer);
}
@Test
void sessionRepositoryCustomizer() {
registerAndRefresh(RedisConfig.class, SessionRepositoryCustomizerConfiguration.class);
ReactiveRedisSessionRepository sessionRepository = this.context.getBean(ReactiveRedisSessionRepository.class);
assertThat(sessionRepository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval",
MAX_INACTIVE_INTERVAL_IN_SECONDS);
}
private void registerAndRefresh(Class<?>... annotatedClasses) {
this.context.register(annotatedClasses);
this.context.refresh();
@@ -231,7 +232,7 @@ class RedisWebSessionConfigurationTests {
static class RedisConfig {
@Bean
public ReactiveRedisConnectionFactory defaultRedisConnectionFactory() {
ReactiveRedisConnectionFactory defaultRedisConnectionFactory() {
return mock(ReactiveRedisConnectionFactory.class);
}
@@ -283,7 +284,7 @@ class RedisWebSessionConfigurationTests {
@Bean
@SpringSessionRedisConnectionFactory
public ReactiveRedisConnectionFactory qualifiedRedisConnectionFactory() {
ReactiveRedisConnectionFactory qualifiedRedisConnectionFactory() {
return mock(ReactiveRedisConnectionFactory.class);
}
@@ -294,7 +295,7 @@ class RedisWebSessionConfigurationTests {
@Bean
@Primary
public ReactiveRedisConnectionFactory primaryRedisConnectionFactory() {
ReactiveRedisConnectionFactory primaryRedisConnectionFactory() {
return mock(ReactiveRedisConnectionFactory.class);
}
@@ -305,13 +306,13 @@ class RedisWebSessionConfigurationTests {
@Bean
@SpringSessionRedisConnectionFactory
public ReactiveRedisConnectionFactory qualifiedRedisConnectionFactory() {
ReactiveRedisConnectionFactory qualifiedRedisConnectionFactory() {
return mock(ReactiveRedisConnectionFactory.class);
}
@Bean
@Primary
public ReactiveRedisConnectionFactory primaryRedisConnectionFactory() {
ReactiveRedisConnectionFactory primaryRedisConnectionFactory() {
return mock(ReactiveRedisConnectionFactory.class);
}
@@ -321,7 +322,7 @@ class RedisWebSessionConfigurationTests {
static class NamedConnectionFactoryRedisConfig {
@Bean
public ReactiveRedisConnectionFactory redisConnectionFactory() {
ReactiveRedisConnectionFactory redisConnectionFactory() {
return mock(ReactiveRedisConnectionFactory.class);
}
@@ -331,7 +332,7 @@ class RedisWebSessionConfigurationTests {
static class MultipleConnectionFactoryRedisConfig {
@Bean
public ReactiveRedisConnectionFactory secondaryRedisConnectionFactory() {
ReactiveRedisConnectionFactory secondaryRedisConnectionFactory() {
return mock(ReactiveRedisConnectionFactory.class);
}
@@ -342,10 +343,28 @@ class RedisWebSessionConfigurationTests {
@Bean
@SuppressWarnings("unchecked")
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return mock(RedisSerializer.class);
}
}
@EnableRedisWebSession
static class SessionRepositoryCustomizerConfiguration {
@Bean
@Order(0)
ReactiveSessionRepositoryCustomizer<ReactiveRedisSessionRepository> sessionRepositoryCustomizerOne() {
return (sessionRepository) -> sessionRepository.setDefaultMaxInactiveInterval(0);
}
@Bean
@Order(1)
ReactiveSessionRepositoryCustomizer<ReactiveRedisSessionRepository> sessionRepositoryCustomizerTwo() {
return (sessionRepository) -> sessionRepository
.setDefaultMaxInactiveInterval(MAX_INACTIVE_INTERVAL_IN_SECONDS);
}
}
}

View File

@@ -75,7 +75,7 @@ To get started with Spring Session, the best place to start is our Sample Applic
|
| {gh-samples-url}spring-session-sample-boot-redis-simple[HttpSession with simple Redis `SessionRepository`]
| Demonstrates how to use Spring Session to replace the `HttpSession` with Redis using `SimpleRedisOperationsSessionRepository`.
| Demonstrates how to use Spring Session to replace the `HttpSession` with Redis using `RedisSessionRepository`.
|
|===
@@ -590,12 +590,12 @@ You can browse the complete link:../../api/[Javadoc] online. The key APIs are de
* <<api-reactivesessionrepository>>
* <<api-enablespringhttpsession>>
* <<api-enablespringwebsession>>
* <<api-redisoperationssessionrepository>>
* <<api-reactiveredisoperationssessionrepository>>
* <<api-redisindexedsessionrepository>>
* <<api-reactiveredissessionrepository>>
* <<api-mapsessionrepository>>
* <<api-reactivemapsessionrepository>>
* <<api-jdbcoperationssessionrepository>>
* <<api-hazelcastsessionrepository>>
* <<api-jdbcindexedsessionrepository>>
* <<api-hazelcastindexedsessionrepository>>
[[api-session]]
=== Using `Session`
@@ -727,31 +727,31 @@ Note that no infrastructure for session expirations is configured for you.
This is because things such as session expiration are highly implementation-dependent.
This means that, if you require cleaning up expired sessions, you are responsible for cleaning up the expired sessions.
[[api-redisoperationssessionrepository]]
=== Using `RedisOperationsSessionRepository`
[[api-redisindexedsessionrepository]]
=== Using `RedisIndexedSessionRepository`
`RedisOperationsSessionRepository` is a `SessionRepository` that is implemented by using Spring Data's `RedisOperations`.
`RedisIndexedSessionRepository` is a `SessionRepository` that is implemented by using Spring Data's `RedisOperations`.
In a web environment, this is typically used in combination with `SessionRepositoryFilter`.
The implementation supports `SessionDestroyedEvent` and `SessionCreatedEvent` through `SessionMessageListener`.
[[api-redisoperationssessionrepository-new]]
==== Instantiating a `RedisOperationsSessionRepository`
[[api-redisindexedsessionrepository-new]]
==== Instantiating a `RedisIndexedSessionRepository`
You can see a typical example of how to create a new instance in the following listing:
====
[source,java,indent=0]
----
include::{indexdoc-tests}[tags=new-redisoperationssessionrepository]
include::{indexdoc-tests}[tags=new-redisindexedsessionrepository]
----
====
For additional information on how to create a `RedisConnectionFactory`, see the Spring Data Redis Reference.
[[api-redisoperationssessionrepository-config]]
[[api-redisindexedsessionrepository-config]]
==== Using `@EnableRedisHttpSession`
In a web environment, the simplest way to create a new `RedisOperationsSessionRepository` is to use `@EnableRedisHttpSession`.
In a web environment, the simplest way to create a new `RedisIndexedSessionRepository` is to use `@EnableRedisHttpSession`.
You can find complete example usage in the <<samples>>.
You can use the following attributes to customize the configuration:
@@ -766,11 +766,11 @@ You can customize the serialization by creating a bean named `springSessionDefau
==== Redis `TaskExecutor`
`RedisOperationsSessionRepository` is subscribed to receive events from Redis by using a `RedisMessageListenerContainer`.
`RedisIndexedSessionRepository` is subscribed to receive events from Redis by using a `RedisMessageListenerContainer`.
You can customize the way those events are dispatched by creating a bean named `springSessionRedisTaskExecutor`, a bean `springSessionRedisSubscriptionExecutor`, or both.
You can find more details on configuring Redis task executors https://docs.spring.io/spring-data-redis/docs/{spring-data-redis-version}/reference/html/#redis:pubsub:subscribe:containers[here].
[[api-redisoperationssessionrepository-storage]]
[[api-redisindexedsessionrepository-storage]]
==== Storage Details
The following sections outline how Redis is updated for each operation.
@@ -819,10 +819,10 @@ In the preceding example, the following statements are true about the session:
The first is `attrName`, with a value of `someAttrValue`.
The second session attribute is named `attrName2`, with a value of `someAttrValue2`.
[[api-redisoperationssessionrepository-writes]]
[[api-redisindexedsessionrepository-writes]]
===== Optimized Writes
The `Session` instances managed by `RedisOperationsSessionRepository` keeps track of the properties that have changed and updates only those.
The `Session` instances managed by `RedisIndexedSessionRepository` keeps track of the properties that have changed and updates only those.
This means that, if an attribute is written once and read many times, we need to write that attribute only once.
For example, assume the `sessionAttr2` session attribute from the lsiting in the preceding section was updated.
The following command would be run upon saving:
@@ -833,7 +833,7 @@ HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe sessionAttr:a
----
====
[[api-redisoperationssessionrepository-expiration]]
[[api-redisindexedsessionrepository-expiration]]
===== Session Expiration
An expiration is associated with each session by using the `EXPIRE` command, based upon the `Session.getMaxInactiveInterval()`.
@@ -852,7 +852,7 @@ An expiration is set on the session itself five minutes after it actually expire
NOTE: The `SessionRepository.findById(String)` method ensures that no expired sessions are returned.
This means that you need not check the expiration before using a session.
Spring Session relies on the delete and expired https://redis.io/topics/notifications[keyspace notifications] from Redis to fire a <<api-redisoperationssessionrepository-sessiondestroyedevent,`SessionDeletedEvent`>> and a <<api-redisoperationssessionrepository-sessiondestroyedevent,`SessionExpiredEvent`>>, respectively.
Spring Session relies on the delete and expired https://redis.io/topics/notifications[keyspace notifications] from Redis to fire a <<api-redisindexedsessionrepository-sessiondestroyedevent,`SessionDeletedEvent`>> and a <<api-redisindexedsessionrepository-sessiondestroyedevent,`SessionExpiredEvent`>>, respectively.
`SessionDeletedEvent` or `SessionExpiredEvent` ensure that resources associated with the `Session` are cleaned up.
For example, when you use Spring Session's WebSocket support, the Redis expired or delete event triggers any WebSocket connections associated with the session to be closed.
@@ -893,12 +893,12 @@ Short of using distributed locks (which would kill our performance), there is no
By simply accessing the key, we ensure that the key is only removed if the TTL on that key is expired.
[[api-redisoperationssessionrepository-sessiondestroyedevent]]
[[api-redisindexedsessionrepository-sessiondestroyedevent]]
==== `SessionDeletedEvent` and `SessionExpiredEvent`
`SessionDeletedEvent` and `SessionExpiredEvent` are both types of `SessionDestroyedEvent`.
`RedisOperationsSessionRepository` supports firing a `SessionDeletedEvent` when a `Session` is deleted or a `SessionExpiredEvent` when a `Session` expires.
`RedisIndexedSessionRepository` supports firing a `SessionDeletedEvent` when a `Session` is deleted or a `SessionExpiredEvent` when a `Session` expires.
This is necessary to ensure resources associated with the `Session` are properly cleaned up.
For example, when integrating with WebSockets, the `SessionDestroyedEvent` is in charge of closing any active WebSocket connections.
@@ -937,15 +937,15 @@ include::{docs-test-resources-dir}docs/HttpSessionConfigurationNoOpConfigureRedi
----
====
[[api-redisoperationssessionrepository-sessioncreatedevent]]
[[api-redisindexedsessionrepository-sessioncreatedevent]]
==== Using `SessionCreatedEvent`
When a session is created, an event is sent to Redis with a channel ID of `spring:session:channel:created:33fdd1b6-b496-4b33-9f7d-df96679d32fe`,
where `33fdd1b6-b496-4b33-9f7d-df96679d32fe` is the session ID. The body of the event is the session that was created.
If registered as a `MessageListener` (the default), `RedisOperationsSessionRepository` then translates the Redis message into a `SessionCreatedEvent`.
If registered as a `MessageListener` (the default), `RedisIndexedSessionRepository` then translates the Redis message into a `SessionCreatedEvent`.
[[api-redisoperationssessionrepository-cli]]
[[api-redisindexedsessionrepository-cli]]
==== Viewing the Session in Redis
After https://redis.io/topics/quickstart[installing redis-cli], you can inspect the values in Redis https://redis.io/commands#hash[using the redis-cli].
@@ -980,30 +980,30 @@ redis 127.0.0.1:6379> hget spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed
----
====
[[api-reactiveredisoperationssessionrepository]]
=== Using `ReactiveRedisOperationsSessionRepository`
[[api-reactiveredissessionrepository]]
=== Using `ReactiveRedisSessionRepository`
`ReactiveRedisOperationsSessionRepository` is a `ReactiveSessionRepository` that is implemented by using Spring Data's `ReactiveRedisOperations`.
`ReactiveRedisSessionRepository` is a `ReactiveSessionRepository` that is implemented by using Spring Data's `ReactiveRedisOperations`.
In a web environment, this is typically used in combination with `WebSessionStore`.
[[api-reactiveredisoperationssessionrepository-new]]
==== Instantiating a `ReactiveRedisOperationsSessionRepository`
[[api-reactiveredissessionrepository-new]]
==== Instantiating a `ReactiveRedisSessionRepository`
The following example shows how to create a new instance:
====
[source,java,indent=0]
----
include::{indexdoc-tests}[tags=new-reactiveredisoperationssessionrepository]
include::{indexdoc-tests}[tags=new-reactiveredissessionrepository]
----
====
For additional information on how to create a `ReactiveRedisConnectionFactory`, see the Spring Data Redis Reference.
[[api-reactiveredisoperationssessionrepository-config]]
[[api-reactiveredissessionrepository-config]]
==== Using `@EnableRedisWebSession`
In a web environment, the simplest way to create a new `ReactiveRedisOperationsSessionRepository` is to use `@EnableRedisWebSession`.
In a web environment, the simplest way to create a new `ReactiveRedisSessionRepository` is to use `@EnableRedisWebSession`.
You can use the following attributes to customize the configuration:
* *maxInactiveIntervalInSeconds*: The amount of time before the session expires, in seconds
@@ -1011,13 +1011,13 @@ You can use the following attributes to customize the configuration:
* *flushMode*: Allows specifying when data is written to Redis. The default is only when `save` is invoked on `ReactiveSessionRepository`.
A value of `FlushMode.IMMEDIATE` writes to Redis as soon as possible.
[[api-reactiveredisoperationssessionrepository-writes]]
[[api-reactiveredissessionrepository-writes]]
===== Optimized Writes
The `Session` instances managed by `ReactiveRedisOperationsSessionRepository` keep track of the properties that have changed and updates only those.
The `Session` instances managed by `ReactiveRedisSessionRepository` keep track of the properties that have changed and updates only those.
This means that, if an attribute is written once and read many times, we need to write that attribute only once.
[[api-reactiveredisoperationssessionrepository-cli]]
[[api-reactiveredissessionrepository-cli]]
==== Viewing the Session in Redis
After https://redis.io/topics/quickstart[installing redis-cli], you can inspect the values in Redis https://redis.io/commands#hash[using the redis-cli].
@@ -1101,31 +1101,31 @@ The `ReactiveMapSessionRepository` allows for persisting `Session` in a `Map`, w
You can use the implementation with a `ConcurrentHashMap` as a testing or convenience mechanism.
Alternatively, you can use it with distributed `Map` implementations, with the requirement that the supplied `Map` must be non-blocking.
[[api-jdbcoperationssessionrepository]]
=== Using `JdbcOperationsSessionRepository`
[[api-jdbcindexedsessionrepository]]
=== Using `JdbcIndexedSessionRepository`
`JdbcOperationsSessionRepository` is a `SessionRepository` implementation that uses Spring's `JdbcOperations` to store sessions in a relational database.
`JdbcIndexedSessionRepository` is a `SessionRepository` implementation that uses Spring's `JdbcOperations` to store sessions in a relational database.
In a web environment, this is typically used in combination with `SessionRepositoryFilter`.
Note that this implementation does not support publishing of session events.
[[api-jdbcoperationssessionrepository-new]]
==== Instantiating a `JdbcOperationsSessionRepository`
[[api-jdbcindexedsessionrepository-new]]
==== Instantiating a `JdbcIndexedSessionRepository`
The following example shows how to create a new instance:
====
[source,java,indent=0]
----
include::{indexdoc-tests}[tags=new-jdbcoperationssessionrepository]
include::{indexdoc-tests}[tags=new-jdbcindexedsessionrepository]
----
====
For additional information on how to create and configure `JdbcTemplate` and `PlatformTransactionManager`, see the https://docs.spring.io/spring/docs/{spring-framework-version}/spring-framework-reference/data-access.html[Spring Framework Reference Documentation].
[[api-jdbcoperationssessionrepository-config]]
[[api-jdbcindexedsessionrepository-config]]
==== Using `@EnableJdbcHttpSession`
In a web environment, the simplest way to create a new `JdbcOperationsSessionRepository` is to use `@EnableJdbcHttpSession`.
In a web environment, the simplest way to create a new `JdbcIndexedSessionRepository` is to use `@EnableJdbcHttpSession`.
You can find complete example usage in the <<samples>>
You can use the following attributes to customize the configuration:
@@ -1142,7 +1142,7 @@ You can customize the default serialization and deserialization of the session b
When working in a typical Spring environment, the default `ConversionService` bean (named `conversionService`) is automatically picked up and used for serialization and deserialization.
However, you can override the default `ConversionService` by providing a bean named `springSessionConversionService`.
[[api-jdbcoperationssessionrepository-storage]]
[[api-jdbcindexedsessionrepository-storage]]
==== Storage Details
By default, this implementation uses `SPRING_SESSION` and `SPRING_SESSION_ATTRIBUTES` tables to store sessions.
@@ -1172,24 +1172,24 @@ include::{session-jdbc-main-resources-dir}org/springframework/session/jdbc/schem
==== Transaction Management
All JDBC operations in `JdbcOperationsSessionRepository` are executed in a transactional manner.
All JDBC operations in `JdbcIndexedSessionRepository` are executed in a transactional manner.
Transactions are executed with propagation set to `REQUIRES_NEW` in order to avoid unexpected behavior due to interference with existing transactions (for example, running a `save` operation in a thread that already participates in a read-only transaction).
[[api-hazelcastsessionrepository]]
=== Using `HazelcastSessionRepository`
[[api-hazelcastindexedsessionrepository]]
=== Using `HazelcastIndexedSessionRepository`
`HazelcastSessionRepository` is a `SessionRepository` implementation that stores sessions in Hazelcast's distributed `IMap`.
`HazelcastIndexedSessionRepository` is a `SessionRepository` implementation that stores sessions in Hazelcast's distributed `IMap`.
In a web environment, this is typically used in combination with `SessionRepositoryFilter`.
[[api-hazelcastsessionrepository-new]]
==== Instantiating a `HazelcastSessionRepository`
[[api-hazelcastindexedsessionrepository-new]]
==== Instantiating a `HazelcastIndexedSessionRepository`
The following example shows how to create a new instance:
====
[source,java,indent=0]
----
include::{indexdoc-tests}[tags=new-hazelcastsessionrepository]
include::{indexdoc-tests}[tags=new-hazelcastindexedsessionrepository]
----
====
@@ -1351,7 +1351,7 @@ The minimum requirements for Spring Session are:
* Java 8+.
* If you run in a Servlet Container (not required), Servlet 3.1+.
* If you use other Spring libraries (not required), the minimum required version is Spring 5.0.x.
* `@EnableRedisHttpSession` requires Redis 2.8+. This is necessary to support <<api-redisoperationssessionrepository-expiration,Session Expiration>>
* `@EnableRedisHttpSession` requires Redis 2.8+. This is necessary to support <<api-redisindexedsessionrepository-expiration,Session Expiration>>
* `@EnableHazelcastHttpSession` requires Hazelcast 3.6+. This is necessary to support <<api-enablehazelcasthttpsession-storage,`FindByIndexNameSessionRepository`>>
NOTE: At its core, Spring Session has a required dependency only on `spring-jcl`.

View File

@@ -30,19 +30,18 @@ import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.mock.web.MockServletContext;
import org.springframework.session.MapSession;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepository;
import org.springframework.session.data.redis.RedisOperationsSessionRepository;
import org.springframework.session.hazelcast.HazelcastSessionRepository;
import org.springframework.session.jdbc.JdbcOperationsSessionRepository;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
@@ -120,32 +119,31 @@ class IndexDocTests {
@Test
@SuppressWarnings("unused")
void newRedisOperationsSessionRepository() {
// tag::new-redisoperationssessionrepository[]
void newRedisIndexedSessionRepository() {
// tag::new-redisindexedsessionrepository[]
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
// ... configure redisTemplate ...
SessionRepository<? extends Session> repository = new RedisOperationsSessionRepository(redisTemplate);
// end::new-redisoperationssessionrepository[]
SessionRepository<? extends Session> repository = new RedisIndexedSessionRepository(redisTemplate);
// end::new-redisindexedsessionrepository[]
}
@Test
@SuppressWarnings("unused")
void newReactiveRedisOperationsSessionRepository() {
void newReactiveRedisSessionRepository() {
LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory();
RedisSerializationContext<String, Object> serializationContext = RedisSerializationContext
.<String, Object>newSerializationContext(new JdkSerializationRedisSerializer()).build();
// tag::new-reactiveredisoperationssessionrepository[]
// tag::new-reactiveredissessionrepository[]
// ... create and configure connectionFactory and serializationContext ...
ReactiveRedisTemplate<String, Object> redisTemplate = new ReactiveRedisTemplate<>(connectionFactory,
serializationContext);
ReactiveSessionRepository<? extends Session> repository = new ReactiveRedisOperationsSessionRepository(
redisTemplate);
// end::new-reactiveredisoperationssessionrepository[]
ReactiveSessionRepository<? extends Session> repository = new ReactiveRedisSessionRepository(redisTemplate);
// end::new-reactiveredissessionrepository[]
}
@Test
@@ -158,25 +156,25 @@ class IndexDocTests {
@Test
@SuppressWarnings("unused")
void newJdbcOperationsSessionRepository() {
// tag::new-jdbcoperationssessionrepository[]
void newJdbcIndexedSessionRepository() {
// tag::new-jdbcindexedsessionrepository[]
JdbcTemplate jdbcTemplate = new JdbcTemplate();
// ... configure JdbcTemplate ...
// ... configure jdbcTemplate ...
PlatformTransactionManager transactionManager = new DataSourceTransactionManager();
TransactionTemplate transactionTemplate = new TransactionTemplate();
// ... configure transactionManager ...
// ... configure transactionTemplate ...
SessionRepository<? extends Session> repository = new JdbcOperationsSessionRepository(jdbcTemplate,
transactionManager);
// end::new-jdbcoperationssessionrepository[]
SessionRepository<? extends Session> repository = new JdbcIndexedSessionRepository(jdbcTemplate,
transactionTemplate);
// end::new-jdbcindexedsessionrepository[]
}
@Test
@SuppressWarnings("unused")
void newHazelcastSessionRepository() {
// tag::new-hazelcastsessionrepository[]
void newHazelcastIndexedSessionRepository() {
// tag::new-hazelcastindexedsessionrepository[]
Config config = new Config();
@@ -184,8 +182,8 @@ class IndexDocTests {
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(config);
HazelcastSessionRepository repository = new HazelcastSessionRepository(hazelcastInstance);
// end::new-hazelcastsessionrepository[]
HazelcastIndexedSessionRepository repository = new HazelcastIndexedSessionRepository(hazelcastInstance);
// end::new-hazelcastindexedsessionrepository[]
}
@Test

View File

@@ -48,13 +48,13 @@ class RedisHttpSessionConfigurationNoOpConfigureRedisActionTests {
// tag::configure-redis-action[]
@Bean
public static ConfigureRedisAction configureRedisAction() {
ConfigureRedisAction configureRedisAction() {
return ConfigureRedisAction.NO_OP;
}
// end::configure-redis-action[]
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisConnectionFactory redisConnectionFactory() {
return mock(RedisConnectionFactory.class);
}

View File

@@ -24,7 +24,7 @@ import com.hazelcast.core.HazelcastInstance;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.hazelcast.HazelcastSessionRepository;
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
import org.springframework.session.hazelcast.PrincipalNameExtractor;
import org.springframework.session.hazelcast.config.annotation.web.http.EnableHazelcastHttpSession;
@@ -36,14 +36,14 @@ public class HazelcastHttpSessionConfig {
@Bean
public HazelcastInstance hazelcastInstance() {
MapAttributeConfig attributeConfig = new MapAttributeConfig()
.setName(HazelcastSessionRepository.PRINCIPAL_NAME_ATTRIBUTE)
.setName(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE)
.setExtractor(PrincipalNameExtractor.class.getName());
Config config = new Config();
config.getMapConfig(HazelcastSessionRepository.DEFAULT_SESSION_MAP_NAME) // <2>
.addMapAttributeConfig(attributeConfig)
.addMapIndexConfig(new MapIndexConfig(HazelcastSessionRepository.PRINCIPAL_NAME_ATTRIBUTE, false));
config.getMapConfig(HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME) // <2>
.addMapAttributeConfig(attributeConfig).addMapIndexConfig(
new MapIndexConfig(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE, false));
return Hazelcast.newHazelcastInstance(config); // <3>
}

View File

@@ -32,12 +32,12 @@ class HttpSessionListenerJavaConfigTests extends AbstractHttpSessionListenerTest
static class MockConfig {
@Bean
public static RedisConnectionFactory redisConnectionFactory() {
static RedisConnectionFactory redisConnectionFactory() {
return AbstractHttpSessionListenerTests.createMockRedisConnection();
}
@Bean
public SecuritySessionDestroyedListener securitySessionDestroyedListener() {
SecuritySessionDestroyedListener securitySessionDestroyedListener() {
return new SecuritySessionDestroyedListener();
}

View File

@@ -30,17 +30,17 @@ import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.MapSession;
import org.springframework.session.hazelcast.HazelcastSessionRepository.HazelcastSession;
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository.HazelcastSession;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Abstract base class for Hazelcast integration tests.
* Base class for {@link HazelcastIndexedSessionRepository} integration tests.
*
* @author Tommy Ludwig
* @author Vedran Pavic
*/
abstract class AbstractHazelcastRepositoryITests {
abstract class AbstractHazelcastIndexedSessionRepositoryITests {
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
@@ -48,7 +48,7 @@ abstract class AbstractHazelcastRepositoryITests {
private HazelcastInstance hazelcastInstance;
@Autowired
private HazelcastSessionRepository repository;
private HazelcastIndexedSessionRepository repository;
@Test
void createAndDestroySession() {
@@ -56,7 +56,7 @@ abstract class AbstractHazelcastRepositoryITests {
String sessionId = sessionToSave.getId();
IMap<String, MapSession> hazelcastMap = this.hazelcastInstance
.getMap(HazelcastSessionRepository.DEFAULT_SESSION_MAP_NAME);
.getMap(HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME);
assertThat(hazelcastMap.size()).isEqualTo(0);

View File

@@ -35,17 +35,16 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
/**
* Integration tests that check the underlying data source - in this case Hazelcast
* Client.
* Integration tests for {@link HazelcastIndexedSessionRepository} using client-server
* topology.
*
* @author Vedran Pavic
* @author Artem Bilan
* @since 1.1
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
class HazelcastClientRepositoryITests extends AbstractHazelcastRepositoryITests {
class ClientServerHazelcastIndexedSessionRepositoryITests extends AbstractHazelcastIndexedSessionRepositoryITests {
private static GenericContainer container = new GenericContainer<>("hazelcast/hazelcast:3.12.2")
.withExposedPorts(5701).withEnv("JAVA_OPTS", "-Dhazelcast.config=/opt/hazelcast/config_ext/hazelcast.xml")
@@ -67,7 +66,7 @@ class HazelcastClientRepositoryITests extends AbstractHazelcastRepositoryITests
static class HazelcastSessionConfig {
@Bean
public HazelcastInstance hazelcastInstance() {
HazelcastInstance hazelcastInstance() {
ClientConfig clientConfig = new ClientConfig();
clientConfig.getNetworkConfig()
.addAddress(container.getContainerIpAddress() + ":" + container.getFirstMappedPort());

View File

@@ -27,8 +27,8 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
/**
* Integration tests that check the underlying data source - in this case Hazelcast
* Server.
* Integration tests for {@link HazelcastIndexedSessionRepository} using embedded
* topology.
*
* @author Tommy Ludwig
* @author Vedran Pavic
@@ -36,14 +36,14 @@ import org.springframework.test.context.web.WebAppConfiguration;
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
class HazelcastServerRepositoryITests extends AbstractHazelcastRepositoryITests {
class EmbeddedHazelcastIndexedSessionRepositoryITests extends AbstractHazelcastIndexedSessionRepositoryITests {
@EnableHazelcastHttpSession
@Configuration
static class HazelcastSessionConfig {
@Bean
public HazelcastInstance hazelcastInstance() {
HazelcastInstance hazelcastInstance() {
return HazelcastITestUtils.embeddedHazelcastServer();
}

View File

@@ -42,7 +42,7 @@ public final class HazelcastITestUtils {
*/
public static HazelcastInstance embeddedHazelcastServer(int port) {
MapAttributeConfig attributeConfig = new MapAttributeConfig()
.setName(HazelcastSessionRepository.PRINCIPAL_NAME_ATTRIBUTE)
.setName(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE)
.setExtractor(PrincipalNameExtractor.class.getName());
Config config = new Config();
@@ -53,8 +53,9 @@ public final class HazelcastITestUtils {
networkConfig.getJoin().getMulticastConfig().setEnabled(false);
config.getMapConfig(HazelcastSessionRepository.DEFAULT_SESSION_MAP_NAME).addMapAttributeConfig(attributeConfig)
.addMapIndexConfig(new MapIndexConfig(HazelcastSessionRepository.PRINCIPAL_NAME_ATTRIBUTE, false));
config.getMapConfig(HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME)
.addMapAttributeConfig(attributeConfig).addMapIndexConfig(
new MapIndexConfig(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE, false));
return Hazelcast.newHazelcastInstance(config);
}

View File

@@ -206,12 +206,12 @@ class EnableHazelcastHttpSessionEventsTests<S extends Session> {
static class HazelcastSessionConfig {
@Bean
public HazelcastInstance embeddedHazelcast() {
HazelcastInstance embeddedHazelcast() {
return HazelcastITestUtils.embeddedHazelcastServer();
}
@Bean
public SessionEventRegistry sessionEventRegistry() {
SessionEventRegistry sessionEventRegistry() {
return new SessionEventRegistry();
}

View File

@@ -72,7 +72,7 @@ public class HazelcastHttpSessionConfigurationXmlTests<S extends Session> {
static class HazelcastSessionXmlConfigCustomMapName {
@Bean
public HazelcastInstance embeddedHazelcast() {
HazelcastInstance embeddedHazelcast() {
Config hazelcastConfig = new ClasspathXmlConfig(
"org/springframework/session/hazelcast/config/annotation/web/http/hazelcast-custom-map-name.xml");
NetworkConfig netConfig = new NetworkConfig();
@@ -111,7 +111,7 @@ public class HazelcastHttpSessionConfigurationXmlTests<S extends Session> {
static class HazelcastSessionXmlConfigCustomMapNameAndIdle {
@Bean
public HazelcastInstance embeddedHazelcast() {
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();

View File

@@ -0,0 +1,477 @@
/*
* Copyright 2014-2019 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
*
* https://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.Set;
import java.util.concurrent.TimeUnit;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import com.hazelcast.core.EntryEvent;
import com.hazelcast.core.HazelcastInstance;
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.ApplicationEventPublisher;
import org.springframework.session.DelegatingIndexResolver;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.FlushMode;
import org.springframework.session.IndexResolver;
import org.springframework.session.MapSession;
import org.springframework.session.PrincipalNameIndexResolver;
import org.springframework.session.SaveMode;
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;
import org.springframework.util.ClassUtils;
/**
* 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);
*
* HazelcastIndexedSessionRepository sessionRepository =
* new HazelcastIndexedSessionRepository(hazelcastInstance);
* </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(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE)
* .setExtractor(PrincipalNameExtractor.class.getName());
*
* Config config = new Config();
*
* config.getMapConfig(HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME)
* .addMapAttributeConfig(attributeConfig)
* .addMapIndexConfig(new MapIndexConfig(
* HazelcastIndexedSessionRepository.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 2.2.0
*/
public class HazelcastIndexedSessionRepository
implements FindByIndexNameSessionRepository<HazelcastIndexedSessionRepository.HazelcastSession>,
EntryAddedListener<String, MapSession>, EntryEvictedListener<String, MapSession>,
EntryRemovedListener<String, MapSession> {
/**
* The default name of map used by Spring Session to store sessions.
*/
public static final String DEFAULT_SESSION_MAP_NAME = "spring:session:sessions";
/**
* The principal name custom attribute name.
*/
public static final String PRINCIPAL_NAME_ATTRIBUTE = "principalName";
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
private static final boolean SUPPORTS_SET_TTL = ClassUtils.hasAtLeastOneMethodWithName(IMap.class, "setTtl");
private static final Log logger = LogFactory.getLog(HazelcastIndexedSessionRepository.class);
private final HazelcastInstance hazelcastInstance;
private ApplicationEventPublisher eventPublisher = (event) -> {
};
/**
* If non-null, this value is used to override
* {@link MapSession#setMaxInactiveInterval(Duration)}.
*/
private Integer defaultMaxInactiveInterval;
private IndexResolver<Session> indexResolver = new DelegatingIndexResolver<>(new PrincipalNameIndexResolver<>());
private String sessionMapName = DEFAULT_SESSION_MAP_NAME;
private FlushMode flushMode = FlushMode.ON_SAVE;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
private IMap<String, MapSession> sessions;
private String sessionListenerId;
/**
* Create a new {@link HazelcastIndexedSessionRepository} instance.
* @param hazelcastInstance the {@link HazelcastInstance} to use for managing sessions
*/
public HazelcastIndexedSessionRepository(HazelcastInstance hazelcastInstance) {
Assert.notNull(hazelcastInstance, "HazelcastInstance must not be null");
this.hazelcastInstance = hazelcastInstance;
}
@PostConstruct
public void init() {
this.sessions = this.hazelcastInstance.getMap(this.sessionMapName);
this.sessionListenerId = this.sessions.addEntryListener(this, true);
}
@PreDestroy
public 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;
}
/**
* Set the {@link IndexResolver} to use.
* @param indexResolver the index resolver
*/
public void setIndexResolver(IndexResolver<Session> indexResolver) {
Assert.notNull(indexResolver, "indexResolver cannot be null");
this.indexResolver = indexResolver;
}
/**
* Set the name of map used to store sessions.
* @param sessionMapName the session map name
*/
public void setSessionMapName(String sessionMapName) {
Assert.hasText(sessionMapName, "Map name must not be empty");
this.sessionMapName = sessionMapName;
}
/**
* Sets the Hazelcast flush mode. Default flush mode is {@link FlushMode#ON_SAVE}.
* @param flushMode the new Hazelcast flush mode
*/
public void setFlushMode(FlushMode flushMode) {
Assert.notNull(flushMode, "flushMode cannot be null");
this.flushMode = flushMode;
}
/**
* Set the save mode.
* @param saveMode the save mode
*/
public void setSaveMode(SaveMode saveMode) {
Assert.notNull(saveMode, "saveMode must not be null");
this.saveMode = saveMode;
}
@Override
public HazelcastSession createSession() {
MapSession cached = new MapSession();
if (this.defaultMaxInactiveInterval != null) {
cached.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
}
HazelcastSession session = new HazelcastSession(cached, true);
session.flushImmediateIfNecessary();
return session;
}
@Override
public void save(HazelcastSession session) {
if (session.isNew) {
this.sessions.set(session.getId(), session.getDelegate(), session.getMaxInactiveInterval().getSeconds(),
TimeUnit.SECONDS);
}
else if (session.sessionIdChanged) {
this.sessions.delete(session.originalId);
session.originalId = session.getId();
this.sessions.set(session.getId(), session.getDelegate(), session.getMaxInactiveInterval().getSeconds(),
TimeUnit.SECONDS);
}
else if (session.hasChanges()) {
SessionUpdateEntryProcessor entryProcessor = new SessionUpdateEntryProcessor();
if (session.lastAccessedTimeChanged) {
entryProcessor.setLastAccessedTime(session.getLastAccessedTime());
}
if (session.maxInactiveIntervalChanged) {
if (SUPPORTS_SET_TTL) {
updateTtl(session);
}
entryProcessor.setMaxInactiveInterval(session.getMaxInactiveInterval());
}
if (!session.delta.isEmpty()) {
entryProcessor.setDelta(new HashMap<>(session.delta));
}
this.sessions.executeOnKey(session.getId(), entryProcessor);
}
session.clearChangeFlags();
}
private void updateTtl(HazelcastSession session) {
this.sessions.setTtl(session.getId(), session.getMaxInactiveInterval().getSeconds(), TimeUnit.SECONDS);
}
@Override
public HazelcastSession findById(String id) {
MapSession saved = this.sessions.get(id);
if (saved == null) {
return null;
}
if (saved.isExpired()) {
deleteById(saved.getId());
return null;
}
return new HazelcastSession(saved, false);
}
@Override
public void deleteById(String id) {
this.sessions.remove(id);
}
@Override
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, false));
}
return sessionMap;
}
@Override
public void entryAdded(EntryEvent<String, MapSession> event) {
MapSession session = event.getValue();
if (session.getId().equals(session.getOriginalId())) {
if (logger.isDebugEnabled()) {
logger.debug("Session created with id: " + session.getId());
}
this.eventPublisher.publishEvent(new SessionCreatedEvent(this, session));
}
}
@Override
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()));
}
@Override
public void entryRemoved(EntryEvent<String, MapSession> event) {
MapSession session = event.getOldValue();
if (session != null) {
if (logger.isDebugEnabled()) {
logger.debug("Session deleted with id: " + session.getId());
}
this.eventPublisher.publishEvent(new SessionDeletedEvent(this, session));
}
}
/**
* 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 isNew;
private boolean sessionIdChanged;
private boolean lastAccessedTimeChanged;
private boolean maxInactiveIntervalChanged;
private String originalId;
private Map<String, Object> delta = new HashMap<>();
HazelcastSession(MapSession cached, boolean isNew) {
this.delegate = cached;
this.isNew = isNew;
this.originalId = cached.getId();
if (this.isNew || (HazelcastIndexedSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
getAttributeNames()
.forEach((attributeName) -> this.delta.put(attributeName, cached.getAttribute(attributeName)));
}
}
@Override
public void setLastAccessedTime(Instant lastAccessedTime) {
this.delegate.setLastAccessedTime(lastAccessedTime);
this.lastAccessedTimeChanged = true;
flushImmediateIfNecessary();
}
@Override
public boolean isExpired() {
return this.delegate.isExpired();
}
@Override
public Instant getCreationTime() {
return this.delegate.getCreationTime();
}
@Override
public String getId() {
return this.delegate.getId();
}
@Override
public String changeSessionId() {
String newSessionId = this.delegate.changeSessionId();
this.sessionIdChanged = true;
return newSessionId;
}
@Override
public Instant getLastAccessedTime() {
return this.delegate.getLastAccessedTime();
}
@Override
public void setMaxInactiveInterval(Duration interval) {
this.delegate.setMaxInactiveInterval(interval);
this.maxInactiveIntervalChanged = true;
flushImmediateIfNecessary();
}
@Override
public Duration getMaxInactiveInterval() {
return this.delegate.getMaxInactiveInterval();
}
@Override
public <T> T getAttribute(String attributeName) {
T attributeValue = this.delegate.getAttribute(attributeName);
if (attributeValue != null
&& HazelcastIndexedSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
this.delta.put(attributeName, attributeValue);
}
return attributeValue;
}
@Override
public Set<String> getAttributeNames() {
return this.delegate.getAttributeNames();
}
@Override
public void setAttribute(String attributeName, Object attributeValue) {
this.delegate.setAttribute(attributeName, attributeValue);
this.delta.put(attributeName, attributeValue);
if (SPRING_SECURITY_CONTEXT.equals(attributeName)) {
Map<String, String> indexes = HazelcastIndexedSessionRepository.this.indexResolver
.resolveIndexesFor(this);
String principal = (attributeValue != null) ? indexes.get(PRINCIPAL_NAME_INDEX_NAME) : null;
this.delegate.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal);
}
flushImmediateIfNecessary();
}
@Override
public void removeAttribute(String attributeName) {
setAttribute(attributeName, null);
}
MapSession getDelegate() {
return this.delegate;
}
boolean hasChanges() {
return (this.lastAccessedTimeChanged || this.maxInactiveIntervalChanged || !this.delta.isEmpty());
}
void clearChangeFlags() {
this.isNew = false;
this.lastAccessedTimeChanged = false;
this.sessionIdChanged = false;
this.maxInactiveIntervalChanged = false;
this.delta.clear();
}
private void flushImmediateIfNecessary() {
if (HazelcastIndexedSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {
HazelcastIndexedSessionRepository.this.save(this);
}
}
}
}

View File

@@ -16,190 +16,33 @@
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.Set;
import java.util.concurrent.TimeUnit;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import com.hazelcast.core.EntryEvent;
import com.hazelcast.core.HazelcastInstance;
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.ApplicationEventPublisher;
import org.springframework.session.DelegatingIndexResolver;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.FlushMode;
import org.springframework.session.IndexResolver;
import org.springframework.session.MapSession;
import org.springframework.session.PrincipalNameIndexResolver;
import org.springframework.session.SaveMode;
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.session.SessionRepository;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* 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);
*
* HazelcastSessionRepository sessionRepository =
* new HazelcastSessionRepository(hazelcastInstance);
* </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(HazelcastSessionRepository.DEFAULT_SESSION_MAP_NAME)
* .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>
* This {@link SessionRepository} implementation is kept in order to support migration to
* {@link HazelcastIndexedSessionRepository} in a backwards compatible manner.
*
* @author Vedran Pavic
* @author Tommy Ludwig
* @author Mark Anderson
* @author Aleksandar Stojsavljevic
* @since 1.3.0
* @deprecated since 2.2.0 in favor of {@link HazelcastIndexedSessionRepository}
*/
public class HazelcastSessionRepository
implements FindByIndexNameSessionRepository<HazelcastSessionRepository.HazelcastSession>,
EntryAddedListener<String, MapSession>, EntryEvictedListener<String, MapSession>,
EntryRemovedListener<String, MapSession> {
@Deprecated
public class HazelcastSessionRepository extends HazelcastIndexedSessionRepository {
/**
* The default name of map used by Spring Session to store sessions.
* Create a new {@link HazelcastSessionRepository} instance.
* @param hazelcastInstance the {@link HazelcastInstance} to use for managing sessions
* @see HazelcastIndexedSessionRepository#HazelcastIndexedSessionRepository(HazelcastInstance)
*/
public static final String DEFAULT_SESSION_MAP_NAME = "spring:session:sessions";
/**
* The principal name custom attribute name.
*/
public static final String PRINCIPAL_NAME_ATTRIBUTE = "principalName";
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
private static final boolean SUPPORTS_SET_TTL = ClassUtils.hasAtLeastOneMethodWithName(IMap.class, "setTtl");
private static final Log logger = LogFactory.getLog(HazelcastSessionRepository.class);
private final HazelcastInstance hazelcastInstance;
private final IndexResolver<HazelcastSession> indexResolver;
private ApplicationEventPublisher eventPublisher = (event) -> {
};
/**
* If non-null, this value is used to override
* {@link MapSession#setMaxInactiveInterval(Duration)}.
*/
private Integer defaultMaxInactiveInterval;
private String sessionMapName = DEFAULT_SESSION_MAP_NAME;
private FlushMode flushMode = FlushMode.ON_SAVE;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
private IMap<String, MapSession> sessions;
private String sessionListenerId;
public HazelcastSessionRepository(HazelcastInstance hazelcastInstance) {
Assert.notNull(hazelcastInstance, "HazelcastInstance must not be null");
this.hazelcastInstance = hazelcastInstance;
this.indexResolver = new DelegatingIndexResolver<>(new PrincipalNameIndexResolver<>());
}
@PostConstruct
public void init() {
this.sessions = this.hazelcastInstance.getMap(this.sessionMapName);
this.sessionListenerId = this.sessions.addEntryListener(this, true);
}
@PreDestroy
public 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;
}
/**
* Set the name of map used to store sessions.
* @param sessionMapName the session map name
*/
public void setSessionMapName(String sessionMapName) {
Assert.hasText(sessionMapName, "Map name must not be empty");
this.sessionMapName = sessionMapName;
super(hazelcastInstance);
}
/**
@@ -214,262 +57,4 @@ public class HazelcastSessionRepository
setFlushMode(hazelcastFlushMode.getFlushMode());
}
/**
* Sets the Hazelcast flush mode. Default flush mode is {@link FlushMode#ON_SAVE}.
* @param flushMode the new Hazelcast flush mode
*/
public void setFlushMode(FlushMode flushMode) {
Assert.notNull(flushMode, "flushMode cannot be null");
this.flushMode = flushMode;
}
/**
* Set the save mode.
* @param saveMode the save mode
*/
public void setSaveMode(SaveMode saveMode) {
Assert.notNull(saveMode, "saveMode must not be null");
this.saveMode = saveMode;
}
@Override
public HazelcastSession createSession() {
MapSession cached = new MapSession();
if (this.defaultMaxInactiveInterval != null) {
cached.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
}
HazelcastSession session = new HazelcastSession(cached, true);
session.flushImmediateIfNecessary();
return session;
}
@Override
public void save(HazelcastSession session) {
if (session.isNew) {
this.sessions.set(session.getId(), session.getDelegate(), session.getMaxInactiveInterval().getSeconds(),
TimeUnit.SECONDS);
}
else if (session.sessionIdChanged) {
this.sessions.delete(session.originalId);
session.originalId = session.getId();
this.sessions.set(session.getId(), session.getDelegate(), session.getMaxInactiveInterval().getSeconds(),
TimeUnit.SECONDS);
}
else if (session.hasChanges()) {
SessionUpdateEntryProcessor entryProcessor = new SessionUpdateEntryProcessor();
if (session.lastAccessedTimeChanged) {
entryProcessor.setLastAccessedTime(session.getLastAccessedTime());
}
if (session.maxInactiveIntervalChanged) {
if (SUPPORTS_SET_TTL) {
updateTtl(session);
}
entryProcessor.setMaxInactiveInterval(session.getMaxInactiveInterval());
}
if (!session.delta.isEmpty()) {
entryProcessor.setDelta(new HashMap<>(session.delta));
}
this.sessions.executeOnKey(session.getId(), entryProcessor);
}
session.clearChangeFlags();
}
private void updateTtl(HazelcastSession session) {
this.sessions.setTtl(session.getId(), session.getMaxInactiveInterval().getSeconds(), TimeUnit.SECONDS);
}
@Override
public HazelcastSession findById(String id) {
MapSession saved = this.sessions.get(id);
if (saved == null) {
return null;
}
if (saved.isExpired()) {
deleteById(saved.getId());
return null;
}
return new HazelcastSession(saved, false);
}
@Override
public void deleteById(String id) {
this.sessions.remove(id);
}
@Override
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, false));
}
return sessionMap;
}
@Override
public void entryAdded(EntryEvent<String, MapSession> event) {
MapSession session = event.getValue();
if (session.getId().equals(session.getOriginalId())) {
if (logger.isDebugEnabled()) {
logger.debug("Session created with id: " + session.getId());
}
this.eventPublisher.publishEvent(new SessionCreatedEvent(this, session));
}
}
@Override
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()));
}
@Override
public void entryRemoved(EntryEvent<String, MapSession> event) {
MapSession session = event.getOldValue();
if (session != null) {
if (logger.isDebugEnabled()) {
logger.debug("Session deleted with id: " + session.getId());
}
this.eventPublisher.publishEvent(new SessionDeletedEvent(this, session));
}
}
/**
* 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 isNew;
private boolean sessionIdChanged;
private boolean lastAccessedTimeChanged;
private boolean maxInactiveIntervalChanged;
private String originalId;
private Map<String, Object> delta = new HashMap<>();
HazelcastSession(MapSession cached, boolean isNew) {
this.delegate = cached;
this.isNew = isNew;
this.originalId = cached.getId();
if (this.isNew || (HazelcastSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
getAttributeNames()
.forEach((attributeName) -> this.delta.put(attributeName, cached.getAttribute(attributeName)));
}
}
@Override
public void setLastAccessedTime(Instant lastAccessedTime) {
this.delegate.setLastAccessedTime(lastAccessedTime);
this.lastAccessedTimeChanged = true;
flushImmediateIfNecessary();
}
@Override
public boolean isExpired() {
return this.delegate.isExpired();
}
@Override
public Instant getCreationTime() {
return this.delegate.getCreationTime();
}
@Override
public String getId() {
return this.delegate.getId();
}
@Override
public String changeSessionId() {
String newSessionId = this.delegate.changeSessionId();
this.sessionIdChanged = true;
return newSessionId;
}
@Override
public Instant getLastAccessedTime() {
return this.delegate.getLastAccessedTime();
}
@Override
public void setMaxInactiveInterval(Duration interval) {
this.delegate.setMaxInactiveInterval(interval);
this.maxInactiveIntervalChanged = true;
flushImmediateIfNecessary();
}
@Override
public Duration getMaxInactiveInterval() {
return this.delegate.getMaxInactiveInterval();
}
@Override
public <T> T getAttribute(String attributeName) {
T attributeValue = this.delegate.getAttribute(attributeName);
if (attributeValue != null && HazelcastSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
this.delta.put(attributeName, attributeValue);
}
return attributeValue;
}
@Override
public Set<String> getAttributeNames() {
return this.delegate.getAttributeNames();
}
@Override
public void setAttribute(String attributeName, Object attributeValue) {
this.delegate.setAttribute(attributeName, attributeValue);
this.delta.put(attributeName, attributeValue);
if (SPRING_SECURITY_CONTEXT.equals(attributeName)) {
Map<String, String> indexes = HazelcastSessionRepository.this.indexResolver.resolveIndexesFor(this);
String principal = (attributeValue != null) ? indexes.get(PRINCIPAL_NAME_INDEX_NAME) : null;
this.delegate.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal);
}
flushImmediateIfNecessary();
}
@Override
public void removeAttribute(String attributeName) {
setAttribute(attributeName, null);
}
MapSession getDelegate() {
return this.delegate;
}
boolean hasChanges() {
return (this.lastAccessedTimeChanged || this.maxInactiveIntervalChanged || !this.delta.isEmpty());
}
void clearChangeFlags() {
this.isNew = false;
this.lastAccessedTimeChanged = false;
this.sessionIdChanged = false;
this.maxInactiveIntervalChanged = false;
this.delta.clear();
}
private void flushImmediateIfNecessary() {
if (HazelcastSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {
HazelcastSessionRepository.this.save(this);
}
}
}
}

View File

@@ -25,13 +25,14 @@ import com.hazelcast.map.AbstractEntryProcessor;
import com.hazelcast.map.EntryProcessor;
import org.springframework.session.MapSession;
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository.HazelcastSession;
/**
* Hazelcast {@link EntryProcessor} responsible for handling updates to session.
*
* @author Vedran Pavic
* @since 1.3.4
* @see HazelcastSessionRepository#save(HazelcastSessionRepository.HazelcastSession)
* @see HazelcastIndexedSessionRepository#save(HazelcastSession)
*/
public class SessionUpdateEntryProcessor extends AbstractEntryProcessor<String, MapSession> implements Offloadable {

View File

@@ -25,11 +25,11 @@ import java.lang.annotation.Target;
import com.hazelcast.core.HazelcastInstance;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.session.hazelcast.HazelcastSessionRepository;
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
/**
* Qualifier annotation for a {@link HazelcastInstance} to be injected in
* {@link HazelcastSessionRepository}.
* {@link HazelcastIndexedSessionRepository}.
*
* @author Vedran Pavic
* @since 2.0.0

View File

@@ -33,7 +33,7 @@ import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
import org.springframework.session.hazelcast.HazelcastFlushMode;
import org.springframework.session.hazelcast.HazelcastSessionRepository;
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
/**
@@ -69,7 +69,7 @@ import org.springframework.session.web.http.SessionRepositoryFilter;
@Target(ElementType.TYPE)
@Documented
@Import(HazelcastHttpSessionConfiguration.class)
@Configuration
@Configuration(proxyBeanMethods = false)
public @interface EnableHazelcastHttpSession {
/**
@@ -81,10 +81,11 @@ public @interface EnableHazelcastHttpSession {
/**
* This is the name of the Map that will be used in Hazelcast to store the session
* data. Default is {@link HazelcastSessionRepository#DEFAULT_SESSION_MAP_NAME}.
* data. Default is
* {@link HazelcastIndexedSessionRepository#DEFAULT_SESSION_MAP_NAME}.
* @return the name of the Map to store the sessions in Hazelcast
*/
String sessionMapName() default HazelcastSessionRepository.DEFAULT_SESSION_MAP_NAME;
String sessionMapName() default HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME;
/**
* Flush mode for the Hazelcast sessions. The default is {@code ON_SAVE} which only

View File

@@ -16,7 +16,9 @@
package org.springframework.session.hazelcast.config.annotation.web.http;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.hazelcast.core.HazelcastInstance;
@@ -29,11 +31,14 @@ import org.springframework.context.annotation.ImportAware;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.session.FlushMode;
import org.springframework.session.IndexResolver;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.config.SessionRepositoryCustomizer;
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.hazelcast.HazelcastIndexedSessionRepository;
import org.springframework.session.hazelcast.config.annotation.SpringSessionHazelcastInstance;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.util.StringUtils;
@@ -53,7 +58,7 @@ public class HazelcastHttpSessionConfiguration extends SpringHttpSessionConfigur
private Integer maxInactiveIntervalInSeconds = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
private String sessionMapName = HazelcastSessionRepository.DEFAULT_SESSION_MAP_NAME;
private String sessionMapName = HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME;
private FlushMode flushMode = FlushMode.ON_SAVE;
@@ -63,16 +68,26 @@ public class HazelcastHttpSessionConfiguration extends SpringHttpSessionConfigur
private ApplicationEventPublisher applicationEventPublisher;
private IndexResolver<Session> indexResolver;
private List<SessionRepositoryCustomizer<HazelcastIndexedSessionRepository>> sessionRepositoryCustomizers;
@Bean
public HazelcastSessionRepository sessionRepository() {
HazelcastSessionRepository sessionRepository = new HazelcastSessionRepository(this.hazelcastInstance);
public HazelcastIndexedSessionRepository sessionRepository() {
HazelcastIndexedSessionRepository sessionRepository = new HazelcastIndexedSessionRepository(
this.hazelcastInstance);
sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
if (this.indexResolver != null) {
sessionRepository.setIndexResolver(this.indexResolver);
}
if (StringUtils.hasText(this.sessionMapName)) {
sessionRepository.setSessionMapName(this.sessionMapName);
}
sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
sessionRepository.setFlushMode(this.flushMode);
sessionRepository.setSaveMode(this.saveMode);
this.sessionRepositoryCustomizers
.forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository));
return sessionRepository;
}
@@ -113,6 +128,17 @@ public class HazelcastHttpSessionConfiguration extends SpringHttpSessionConfigur
this.applicationEventPublisher = applicationEventPublisher;
}
@Autowired(required = false)
public void setIndexResolver(IndexResolver<Session> indexResolver) {
this.indexResolver = indexResolver;
}
@Autowired(required = false)
public void setSessionRepositoryCustomizer(
ObjectProvider<SessionRepositoryCustomizer<HazelcastIndexedSessionRepository>> sessionRepositoryCustomizers) {
this.sessionRepositoryCustomizers = sessionRepositoryCustomizers.orderedStream().collect(Collectors.toList());
}
@Override
@SuppressWarnings("deprecation")
public void setImportMetadata(AnnotationMetadata importMetadata) {

View File

@@ -39,7 +39,7 @@ import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.FlushMode;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.hazelcast.HazelcastSessionRepository.HazelcastSession;
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository.HazelcastSession;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
@@ -57,12 +57,12 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
/**
* Tests for {@link HazelcastSessionRepository}.
* Tests for {@link HazelcastIndexedSessionRepository}.
*
* @author Vedran Pavic
* @author Aleksandar Stojsavljevic
*/
class HazelcastSessionRepositoryTests {
class HazelcastIndexedSessionRepositoryTests {
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
@@ -71,18 +71,18 @@ class HazelcastSessionRepositoryTests {
@SuppressWarnings("unchecked")
private IMap<String, MapSession> sessions = mock(IMap.class);
private HazelcastSessionRepository repository;
private HazelcastIndexedSessionRepository repository;
@BeforeEach
void setUp() {
given(this.hazelcastInstance.<String, MapSession>getMap(anyString())).willReturn(this.sessions);
this.repository = new HazelcastSessionRepository(this.hazelcastInstance);
this.repository = new HazelcastIndexedSessionRepository(this.hazelcastInstance);
this.repository.init();
}
@Test
void constructorNullHazelcastInstance() {
assertThatIllegalArgumentException().isThrownBy(() -> new HazelcastSessionRepository(null))
assertThatIllegalArgumentException().isThrownBy(() -> new HazelcastIndexedSessionRepository(null))
.withMessage("HazelcastInstance must not be null");
}

View File

@@ -26,10 +26,14 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.annotation.Order;
import org.springframework.session.FlushMode;
import org.springframework.session.IndexResolver;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.config.SessionRepositoryCustomizer;
import org.springframework.session.hazelcast.HazelcastFlushMode;
import org.springframework.session.hazelcast.HazelcastSessionRepository;
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
import org.springframework.session.hazelcast.config.annotation.SpringSessionHazelcastInstance;
import org.springframework.test.util.ReflectionTestUtils;
@@ -45,7 +49,6 @@ import static org.mockito.Mockito.mock;
* @author Vedran Pavic
* @author Aleksandar Stojsavljevic
*/
@SuppressWarnings("deprecation")
class HazelcastHttpSessionConfigurationTests {
private static final String MAP_NAME = "spring:test:sessions";
@@ -72,14 +75,14 @@ class HazelcastHttpSessionConfigurationTests {
void defaultConfiguration() {
registerAndRefresh(DefaultConfiguration.class);
assertThat(this.context.getBean(HazelcastSessionRepository.class)).isNotNull();
assertThat(this.context.getBean(HazelcastIndexedSessionRepository.class)).isNotNull();
}
@Test
void customTableName() {
registerAndRefresh(CustomSessionMapNameConfiguration.class);
HazelcastSessionRepository repository = this.context.getBean(HazelcastSessionRepository.class);
HazelcastIndexedSessionRepository repository = this.context.getBean(HazelcastIndexedSessionRepository.class);
HazelcastHttpSessionConfiguration configuration = this.context.getBean(HazelcastHttpSessionConfiguration.class);
assertThat(repository).isNotNull();
assertThat(ReflectionTestUtils.getField(configuration, "sessionMapName")).isEqualTo(MAP_NAME);
@@ -89,7 +92,7 @@ class HazelcastHttpSessionConfigurationTests {
void setCustomSessionMapName() {
registerAndRefresh(BaseConfiguration.class, CustomSessionMapNameSetConfiguration.class);
HazelcastSessionRepository repository = this.context.getBean(HazelcastSessionRepository.class);
HazelcastIndexedSessionRepository repository = this.context.getBean(HazelcastIndexedSessionRepository.class);
HazelcastHttpSessionConfiguration configuration = this.context.getBean(HazelcastHttpSessionConfiguration.class);
assertThat(repository).isNotNull();
assertThat(ReflectionTestUtils.getField(configuration, "sessionMapName")).isEqualTo(MAP_NAME);
@@ -99,7 +102,7 @@ class HazelcastHttpSessionConfigurationTests {
void setCustomMaxInactiveIntervalInSeconds() {
registerAndRefresh(BaseConfiguration.class, CustomMaxInactiveIntervalInSecondsSetConfiguration.class);
HazelcastSessionRepository repository = this.context.getBean(HazelcastSessionRepository.class);
HazelcastIndexedSessionRepository repository = this.context.getBean(HazelcastIndexedSessionRepository.class);
assertThat(repository).isNotNull();
assertThat(ReflectionTestUtils.getField(repository, "defaultMaxInactiveInterval"))
.isEqualTo(MAX_INACTIVE_INTERVAL_IN_SECONDS);
@@ -109,7 +112,7 @@ class HazelcastHttpSessionConfigurationTests {
void customMaxInactiveIntervalInSeconds() {
registerAndRefresh(CustomMaxInactiveIntervalInSecondsConfiguration.class);
HazelcastSessionRepository repository = this.context.getBean(HazelcastSessionRepository.class);
HazelcastIndexedSessionRepository repository = this.context.getBean(HazelcastIndexedSessionRepository.class);
assertThat(repository).isNotNull();
assertThat(ReflectionTestUtils.getField(repository, "defaultMaxInactiveInterval"))
.isEqualTo(MAX_INACTIVE_INTERVAL_IN_SECONDS);
@@ -119,7 +122,7 @@ class HazelcastHttpSessionConfigurationTests {
void customFlushImmediately() {
registerAndRefresh(CustomFlushImmediatelyConfiguration.class);
HazelcastSessionRepository repository = this.context.getBean(HazelcastSessionRepository.class);
HazelcastIndexedSessionRepository repository = this.context.getBean(HazelcastIndexedSessionRepository.class);
assertThat(repository).isNotNull();
assertThat(ReflectionTestUtils.getField(repository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
}
@@ -128,7 +131,7 @@ class HazelcastHttpSessionConfigurationTests {
void customFlushImmediatelyLegacy() {
registerAndRefresh(CustomFlushImmediatelyLegacyConfiguration.class);
HazelcastSessionRepository repository = this.context.getBean(HazelcastSessionRepository.class);
HazelcastIndexedSessionRepository repository = this.context.getBean(HazelcastIndexedSessionRepository.class);
assertThat(repository).isNotNull();
assertThat(ReflectionTestUtils.getField(repository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
}
@@ -137,7 +140,7 @@ class HazelcastHttpSessionConfigurationTests {
void setCustomFlushImmediately() {
registerAndRefresh(BaseConfiguration.class, CustomFlushImmediatelySetConfiguration.class);
HazelcastSessionRepository repository = this.context.getBean(HazelcastSessionRepository.class);
HazelcastIndexedSessionRepository repository = this.context.getBean(HazelcastIndexedSessionRepository.class);
assertThat(repository).isNotNull();
assertThat(ReflectionTestUtils.getField(repository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
}
@@ -146,7 +149,7 @@ class HazelcastHttpSessionConfigurationTests {
void setCustomFlushImmediatelyLegacy() {
registerAndRefresh(BaseConfiguration.class, CustomFlushImmediatelySetLegacyConfiguration.class);
HazelcastSessionRepository repository = this.context.getBean(HazelcastSessionRepository.class);
HazelcastIndexedSessionRepository repository = this.context.getBean(HazelcastIndexedSessionRepository.class);
assertThat(repository).isNotNull();
assertThat(ReflectionTestUtils.getField(repository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
}
@@ -154,22 +157,22 @@ class HazelcastHttpSessionConfigurationTests {
@Test
void customSaveModeAnnotation() {
registerAndRefresh(BaseConfiguration.class, CustomSaveModeExpressionAnnotationConfiguration.class);
assertThat(this.context.getBean(HazelcastSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
SaveMode.ALWAYS);
assertThat(this.context.getBean(HazelcastIndexedSessionRepository.class))
.hasFieldOrPropertyWithValue("saveMode", SaveMode.ALWAYS);
}
@Test
void customSaveModeSetter() {
registerAndRefresh(BaseConfiguration.class, CustomSaveModeExpressionSetterConfiguration.class);
assertThat(this.context.getBean(HazelcastSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
SaveMode.ALWAYS);
assertThat(this.context.getBean(HazelcastIndexedSessionRepository.class))
.hasFieldOrPropertyWithValue("saveMode", SaveMode.ALWAYS);
}
@Test
void qualifiedHazelcastInstanceConfiguration() {
registerAndRefresh(QualifiedHazelcastInstanceConfiguration.class);
HazelcastSessionRepository repository = this.context.getBean(HazelcastSessionRepository.class);
HazelcastIndexedSessionRepository repository = this.context.getBean(HazelcastIndexedSessionRepository.class);
HazelcastInstance hazelcastInstance = this.context.getBean("qualifiedHazelcastInstance",
HazelcastInstance.class);
assertThat(repository).isNotNull();
@@ -182,7 +185,7 @@ class HazelcastHttpSessionConfigurationTests {
void primaryHazelcastInstanceConfiguration() {
registerAndRefresh(PrimaryHazelcastInstanceConfiguration.class);
HazelcastSessionRepository repository = this.context.getBean(HazelcastSessionRepository.class);
HazelcastIndexedSessionRepository repository = this.context.getBean(HazelcastIndexedSessionRepository.class);
HazelcastInstance hazelcastInstance = this.context.getBean("primaryHazelcastInstance", HazelcastInstance.class);
assertThat(repository).isNotNull();
assertThat(hazelcastInstance).isNotNull();
@@ -194,7 +197,7 @@ class HazelcastHttpSessionConfigurationTests {
void qualifiedAndPrimaryHazelcastInstanceConfiguration() {
registerAndRefresh(QualifiedAndPrimaryHazelcastInstanceConfiguration.class);
HazelcastSessionRepository repository = this.context.getBean(HazelcastSessionRepository.class);
HazelcastIndexedSessionRepository repository = this.context.getBean(HazelcastIndexedSessionRepository.class);
HazelcastInstance hazelcastInstance = this.context.getBean("qualifiedHazelcastInstance",
HazelcastInstance.class);
assertThat(repository).isNotNull();
@@ -207,7 +210,7 @@ class HazelcastHttpSessionConfigurationTests {
void namedHazelcastInstanceConfiguration() {
registerAndRefresh(NamedHazelcastInstanceConfiguration.class);
HazelcastSessionRepository repository = this.context.getBean(HazelcastSessionRepository.class);
HazelcastIndexedSessionRepository repository = this.context.getBean(HazelcastIndexedSessionRepository.class);
HazelcastInstance hazelcastInstance = this.context.getBean("hazelcastInstance", HazelcastInstance.class);
assertThat(repository).isNotNull();
assertThat(hazelcastInstance).isNotNull();
@@ -222,6 +225,26 @@ class HazelcastHttpSessionConfigurationTests {
.withMessageContaining("expected single matching bean but found 2");
}
@Test
void customIndexResolverConfiguration() {
registerAndRefresh(CustomIndexResolverConfiguration.class);
HazelcastIndexedSessionRepository repository = this.context.getBean(HazelcastIndexedSessionRepository.class);
@SuppressWarnings("unchecked")
IndexResolver<Session> indexResolver = this.context.getBean(IndexResolver.class);
assertThat(repository).isNotNull();
assertThat(indexResolver).isNotNull();
assertThat(repository).hasFieldOrPropertyWithValue("indexResolver", indexResolver);
}
@Test
void sessionRepositoryCustomizer() {
registerAndRefresh(SessionRepositoryCustomizerConfiguration.class);
HazelcastIndexedSessionRepository sessionRepository = this.context
.getBean(HazelcastIndexedSessionRepository.class);
assertThat(sessionRepository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval",
MAX_INACTIVE_INTERVAL_IN_SECONDS);
}
private void registerAndRefresh(Class<?>... annotatedClasses) {
this.context.register(annotatedClasses);
this.context.refresh();
@@ -239,7 +262,7 @@ class HazelcastHttpSessionConfigurationTests {
static IMap<Object, Object> defaultHazelcastInstanceSessions = mock(IMap.class);
@Bean
public HazelcastInstance defaultHazelcastInstance() {
HazelcastInstance defaultHazelcastInstance() {
HazelcastInstance hazelcastInstance = mock(HazelcastInstance.class);
given(hazelcastInstance.getMap(anyString())).willReturn(defaultHazelcastInstanceSessions);
return hazelcastInstance;
@@ -293,6 +316,7 @@ class HazelcastHttpSessionConfigurationTests {
}
@Configuration
@SuppressWarnings("deprecation")
static class CustomFlushImmediatelySetLegacyConfiguration extends HazelcastHttpSessionConfiguration {
CustomFlushImmediatelySetLegacyConfiguration() {
@@ -323,6 +347,7 @@ class HazelcastHttpSessionConfigurationTests {
@Configuration
@EnableHazelcastHttpSession(hazelcastFlushMode = HazelcastFlushMode.IMMEDIATE)
@SuppressWarnings("deprecation")
static class CustomFlushImmediatelyLegacyConfiguration extends BaseConfiguration {
}
@@ -336,7 +361,7 @@ class HazelcastHttpSessionConfigurationTests {
@Bean
@SpringSessionHazelcastInstance
public HazelcastInstance qualifiedHazelcastInstance() {
HazelcastInstance qualifiedHazelcastInstance() {
HazelcastInstance hazelcastInstance = mock(HazelcastInstance.class);
given(hazelcastInstance.getMap(anyString())).willReturn(qualifiedHazelcastInstanceSessions);
return hazelcastInstance;
@@ -353,7 +378,7 @@ class HazelcastHttpSessionConfigurationTests {
@Bean
@Primary
public HazelcastInstance primaryHazelcastInstance() {
HazelcastInstance primaryHazelcastInstance() {
HazelcastInstance hazelcastInstance = mock(HazelcastInstance.class);
given(hazelcastInstance.getMap(anyString())).willReturn(primaryHazelcastInstanceSessions);
return hazelcastInstance;
@@ -373,7 +398,7 @@ class HazelcastHttpSessionConfigurationTests {
@Bean
@SpringSessionHazelcastInstance
public HazelcastInstance qualifiedHazelcastInstance() {
HazelcastInstance qualifiedHazelcastInstance() {
HazelcastInstance hazelcastInstance = mock(HazelcastInstance.class);
given(hazelcastInstance.getMap(anyString())).willReturn(qualifiedHazelcastInstanceSessions);
return hazelcastInstance;
@@ -381,7 +406,7 @@ class HazelcastHttpSessionConfigurationTests {
@Bean
@Primary
public HazelcastInstance primaryHazelcastInstance() {
HazelcastInstance primaryHazelcastInstance() {
HazelcastInstance hazelcastInstance = mock(HazelcastInstance.class);
given(hazelcastInstance.getMap(anyString())).willReturn(primaryHazelcastInstanceSessions);
return hazelcastInstance;
@@ -397,7 +422,7 @@ class HazelcastHttpSessionConfigurationTests {
static IMap<Object, Object> hazelcastInstanceSessions = mock(IMap.class);
@Bean
public HazelcastInstance hazelcastInstance() {
HazelcastInstance hazelcastInstance() {
HazelcastInstance hazelcastInstance = mock(HazelcastInstance.class);
given(hazelcastInstance.getMap(anyString())).willReturn(hazelcastInstanceSessions);
return hazelcastInstance;
@@ -413,7 +438,7 @@ class HazelcastHttpSessionConfigurationTests {
static IMap<Object, Object> secondaryHazelcastInstanceSessions = mock(IMap.class);
@Bean
public HazelcastInstance secondaryHazelcastInstance() {
HazelcastInstance secondaryHazelcastInstance() {
HazelcastInstance hazelcastInstance = mock(HazelcastInstance.class);
given(hazelcastInstance.getMap(anyString())).willReturn(secondaryHazelcastInstanceSessions);
return hazelcastInstance;
@@ -421,4 +446,33 @@ class HazelcastHttpSessionConfigurationTests {
}
@EnableHazelcastHttpSession
static class CustomIndexResolverConfiguration extends BaseConfiguration {
@Bean
@SuppressWarnings("unchecked")
IndexResolver<Session> indexResolver() {
return mock(IndexResolver.class);
}
}
@EnableHazelcastHttpSession
static class SessionRepositoryCustomizerConfiguration extends BaseConfiguration {
@Bean
@Order(0)
SessionRepositoryCustomizer<HazelcastIndexedSessionRepository> sessionRepositoryCustomizerOne() {
return (sessionRepository) -> sessionRepository.setDefaultMaxInactiveInterval(0);
}
@Bean
@Order(1)
SessionRepositoryCustomizer<HazelcastIndexedSessionRepository> sessionRepositoryCustomizerTwo() {
return (sessionRepository) -> sessionRepository
.setDefaultMaxInactiveInterval(MAX_INACTIVE_INTERVAL_IN_SECONDS);
}
}
}

View File

@@ -16,6 +16,7 @@ dependencies {
integrationTestCompile "com.h2database:h2"
integrationTestCompile "com.ibm.db2:jcc"
integrationTestCompile "com.microsoft.sqlserver:mssql-jdbc"
integrationTestCompile "com.oracle.ojdbc:ojdbc8"
integrationTestCompile "com.zaxxer:HikariCP"
integrationTestCompile "mysql:mysql-connector-java"
integrationTestCompile "org.apache.derby:derby"
@@ -29,3 +30,7 @@ dependencies {
integrationTestCompile "org.testcontainers:oracle-xe"
integrationTestCompile "org.testcontainers:postgresql"
}
integrationTest {
maxParallelForks = 4
}

View File

@@ -26,18 +26,17 @@ import org.springframework.jdbc.datasource.init.DataSourceInitializer;
import org.springframework.jdbc.datasource.init.DatabasePopulator;
/**
* Abstract base class for Testcontainers based {@link JdbcOperationsSessionRepository}
* integration tests.
* Base class for Testcontainers based {@link JdbcIndexedSessionRepository} integration
* tests.
*
* @author Vedran Pavic
*/
abstract class AbstractContainerJdbcOperationsSessionRepositoryITests
extends AbstractJdbcOperationsSessionRepositoryITests {
abstract class AbstractContainerJdbcIndexedSessionRepositoryITests extends AbstractJdbcIndexedSessionRepositoryITests {
static class BaseContainerConfig extends BaseConfig {
@Bean
public HikariDataSource dataSource(JdbcDatabaseContainer databaseContainer) {
HikariDataSource dataSource(JdbcDatabaseContainer databaseContainer) {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(databaseContainer.getJdbcUrl());
dataSource.setUsername(databaseContainer.getUsername());
@@ -46,7 +45,7 @@ abstract class AbstractContainerJdbcOperationsSessionRepositoryITests
}
@Bean
public DataSourceInitializer dataSourceInitializer(DataSource dataSource, DatabasePopulator databasePopulator) {
DataSourceInitializer dataSourceInitializer(DataSource dataSource, DatabasePopulator databasePopulator) {
DataSourceInitializer initializer = new DataSourceInitializer();
initializer.setDataSource(dataSource);
initializer.setDatabasePopulator(databasePopulator);

View File

@@ -38,6 +38,7 @@ import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.MapSession;
import org.springframework.session.jdbc.JdbcIndexedSessionRepository.JdbcSession;
import org.springframework.session.jdbc.config.annotation.web.http.EnableJdbcHttpSession;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.transaction.PlatformTransactionManager;
@@ -46,18 +47,18 @@ import org.springframework.transaction.annotation.Transactional;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Abstract base class for {@link JdbcOperationsSessionRepository} integration tests.
* Base class for {@link JdbcIndexedSessionRepository} integration tests.
*
* @author Vedran Pavic
*/
abstract class AbstractJdbcOperationsSessionRepositoryITests {
abstract class AbstractJdbcIndexedSessionRepositoryITests {
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
private static final String INDEX_NAME = FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME;
@Autowired
private JdbcOperationsSessionRepository repository;
private JdbcIndexedSessionRepository repository;
private SecurityContext context;
@@ -76,10 +77,10 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
@Test
void saveWhenNoAttributesThenCanBeFound() {
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
JdbcSession toSave = this.repository.createSession();
this.repository.save(toSave);
JdbcOperationsSessionRepository.JdbcSession session = this.repository.findById(toSave.getId());
JdbcSession session = this.repository.findById(toSave.getId());
assertThat(session).isNotNull();
assertThat(session.isChanged()).isFalse();
@@ -90,7 +91,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
void saves() {
String username = "saves-" + System.currentTimeMillis();
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
JdbcSession toSave = this.repository.createSession();
String expectedAttributeName = "a";
String expectedAttributeValue = "b";
toSave.setAttribute(expectedAttributeName, expectedAttributeValue);
@@ -103,7 +104,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
this.repository.save(toSave);
JdbcOperationsSessionRepository.JdbcSession session = this.repository.findById(toSave.getId());
JdbcSession session = this.repository.findById(toSave.getId());
assertThat(session.getId()).isEqualTo(toSave.getId());
assertThat(session.isChanged()).isFalse();
@@ -120,14 +121,14 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
@Test
@Transactional(readOnly = true)
void savesInReadOnlyTransaction() {
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
JdbcSession toSave = this.repository.createSession();
this.repository.save(toSave);
}
@Test
void putAllOnSingleAttrDoesNotRemoveOld() {
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
JdbcSession toSave = this.repository.createSession();
toSave.setAttribute("a", "b");
this.repository.save(toSave);
@@ -138,7 +139,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
this.repository.save(toSave);
toSave = this.repository.findById(toSave.getId());
JdbcOperationsSessionRepository.JdbcSession session = this.repository.findById(toSave.getId());
JdbcSession session = this.repository.findById(toSave.getId());
assertThat(session.isChanged()).isFalse();
assertThat(session.getDelta()).isEmpty();
assertThat(session.getAttributeNames().size()).isEqualTo(2);
@@ -150,7 +151,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
@Test
void updateLastAccessedTime() {
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
JdbcSession toSave = this.repository.createSession();
toSave.setLastAccessedTime(Instant.now().minusSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS + 1));
this.repository.save(toSave);
@@ -159,7 +160,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
toSave.setLastAccessedTime(lastAccessedTime);
this.repository.save(toSave);
JdbcOperationsSessionRepository.JdbcSession session = this.repository.findById(toSave.getId());
JdbcSession session = this.repository.findById(toSave.getId());
assertThat(session).isNotNull();
assertThat(session.isChanged()).isFalse();
@@ -172,13 +173,13 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
@Test
void findByPrincipalName() {
String principalName = "findByPrincipalName" + UUID.randomUUID();
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
JdbcSession toSave = this.repository.createSession();
toSave.setAttribute(INDEX_NAME, principalName);
this.repository.save(toSave);
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName = this.repository
.findByIndexNameAndIndexValue(INDEX_NAME, principalName);
Map<String, JdbcSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
principalName);
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
@@ -194,15 +195,15 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
@Test
void findByPrincipalNameExpireRemovesIndex() {
String principalName = "findByPrincipalNameExpireRemovesIndex" + UUID.randomUUID();
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
JdbcSession toSave = this.repository.createSession();
toSave.setAttribute(INDEX_NAME, principalName);
toSave.setLastAccessedTime(Instant.now().minusSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS + 1));
this.repository.save(toSave);
this.repository.cleanUpExpiredSessions();
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName = this.repository
.findByIndexNameAndIndexValue(INDEX_NAME, principalName);
Map<String, JdbcSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
principalName);
assertThat(findByPrincipalName).hasSize(0);
assertThat(findByPrincipalName.keySet()).doesNotContain(toSave.getId());
@@ -211,7 +212,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
@Test
void findByPrincipalNameNoPrincipalNameChange() {
String principalName = "findByPrincipalNameNoPrincipalNameChange" + UUID.randomUUID();
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
JdbcSession toSave = this.repository.createSession();
toSave.setAttribute(INDEX_NAME, principalName);
this.repository.save(toSave);
@@ -219,8 +220,8 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
toSave.setAttribute("other", "value");
this.repository.save(toSave);
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName = this.repository
.findByIndexNameAndIndexValue(INDEX_NAME, principalName);
Map<String, JdbcSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
principalName);
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
@@ -233,7 +234,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
@Test
void findByPrincipalNameNoPrincipalNameChangeReload() {
String principalName = "findByPrincipalNameNoPrincipalNameChangeReload" + UUID.randomUUID();
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
JdbcSession toSave = this.repository.createSession();
toSave.setAttribute(INDEX_NAME, principalName);
this.repository.save(toSave);
@@ -243,8 +244,8 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
toSave.setAttribute("other", "value");
this.repository.save(toSave);
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName = this.repository
.findByIndexNameAndIndexValue(INDEX_NAME, principalName);
Map<String, JdbcSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
principalName);
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
@@ -257,7 +258,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
@Test
void findByDeletedPrincipalName() {
String principalName = "findByDeletedPrincipalName" + UUID.randomUUID();
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
JdbcSession toSave = this.repository.createSession();
toSave.setAttribute(INDEX_NAME, principalName);
this.repository.save(toSave);
@@ -265,8 +266,8 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
toSave.setAttribute(INDEX_NAME, null);
this.repository.save(toSave);
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName = this.repository
.findByIndexNameAndIndexValue(INDEX_NAME, principalName);
Map<String, JdbcSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
principalName);
assertThat(findByPrincipalName).isEmpty();
}
@@ -275,7 +276,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
void findByChangedPrincipalName() {
String principalName = "findByChangedPrincipalName" + UUID.randomUUID();
String principalNameChanged = "findByChangedPrincipalName" + UUID.randomUUID();
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
JdbcSession toSave = this.repository.createSession();
toSave.setAttribute(INDEX_NAME, principalName);
this.repository.save(toSave);
@@ -283,8 +284,8 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
toSave.setAttribute(INDEX_NAME, principalNameChanged);
this.repository.save(toSave);
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName = this.repository
.findByIndexNameAndIndexValue(INDEX_NAME, principalName);
Map<String, JdbcSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
principalName);
assertThat(findByPrincipalName).isEmpty();
findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, principalNameChanged);
@@ -300,17 +301,17 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
@Test
void findByDeletedPrincipalNameReload() {
String principalName = "findByDeletedPrincipalName" + UUID.randomUUID();
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
JdbcSession toSave = this.repository.createSession();
toSave.setAttribute(INDEX_NAME, principalName);
this.repository.save(toSave);
JdbcOperationsSessionRepository.JdbcSession getSession = this.repository.findById(toSave.getId());
JdbcSession getSession = this.repository.findById(toSave.getId());
getSession.setAttribute(INDEX_NAME, null);
this.repository.save(getSession);
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName = this.repository
.findByIndexNameAndIndexValue(INDEX_NAME, principalName);
Map<String, JdbcSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
principalName);
assertThat(findByPrincipalName).isEmpty();
}
@@ -319,18 +320,18 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
void findByChangedPrincipalNameReload() {
String principalName = "findByChangedPrincipalName" + UUID.randomUUID();
String principalNameChanged = "findByChangedPrincipalName" + UUID.randomUUID();
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
JdbcSession toSave = this.repository.createSession();
toSave.setAttribute(INDEX_NAME, principalName);
this.repository.save(toSave);
JdbcOperationsSessionRepository.JdbcSession getSession = this.repository.findById(toSave.getId());
JdbcSession getSession = this.repository.findById(toSave.getId());
getSession.setAttribute(INDEX_NAME, principalNameChanged);
this.repository.save(getSession);
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName = this.repository
.findByIndexNameAndIndexValue(INDEX_NAME, principalName);
Map<String, JdbcSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
principalName);
assertThat(findByPrincipalName).isEmpty();
findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, principalNameChanged);
@@ -345,13 +346,13 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
@Test
void findBySecurityPrincipalName() {
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
JdbcSession toSave = this.repository.createSession();
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
this.repository.save(toSave);
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName = this.repository
.findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName());
Map<String, JdbcSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
getSecurityName());
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
@@ -366,15 +367,15 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
@Test
void findBySecurityPrincipalNameExpireRemovesIndex() {
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
JdbcSession toSave = this.repository.createSession();
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
toSave.setLastAccessedTime(Instant.now().minusSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS + 1));
this.repository.save(toSave);
this.repository.cleanUpExpiredSessions();
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName = this.repository
.findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName());
Map<String, JdbcSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
getSecurityName());
assertThat(findByPrincipalName).hasSize(0);
assertThat(findByPrincipalName.keySet()).doesNotContain(toSave.getId());
@@ -382,7 +383,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
@Test
void findByPrincipalNameNoSecurityPrincipalNameChange() {
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
JdbcSession toSave = this.repository.createSession();
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
this.repository.save(toSave);
@@ -390,8 +391,8 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
toSave.setAttribute("other", "value");
this.repository.save(toSave);
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName = this.repository
.findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName());
Map<String, JdbcSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
getSecurityName());
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
@@ -403,7 +404,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
@Test
void findByPrincipalNameNoSecurityPrincipalNameChangeReload() {
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
JdbcSession toSave = this.repository.createSession();
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
this.repository.save(toSave);
@@ -413,8 +414,8 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
toSave.setAttribute("other", "value");
this.repository.save(toSave);
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName = this.repository
.findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName());
Map<String, JdbcSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
getSecurityName());
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
@@ -426,7 +427,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
@Test
void findByDeletedSecurityPrincipalName() {
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
JdbcSession toSave = this.repository.createSession();
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
this.repository.save(toSave);
@@ -434,15 +435,15 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
toSave.setAttribute(SPRING_SECURITY_CONTEXT, null);
this.repository.save(toSave);
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName = this.repository
.findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName());
Map<String, JdbcSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
getSecurityName());
assertThat(findByPrincipalName).isEmpty();
}
@Test
void findByChangedSecurityPrincipalName() {
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
JdbcSession toSave = this.repository.createSession();
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
this.repository.save(toSave);
@@ -450,8 +451,8 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.changedContext);
this.repository.save(toSave);
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName = this.repository
.findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName());
Map<String, JdbcSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
getSecurityName());
assertThat(findByPrincipalName).isEmpty();
findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, getChangedSecurityName());
@@ -466,35 +467,35 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
@Test
void findByDeletedSecurityPrincipalNameReload() {
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
JdbcSession toSave = this.repository.createSession();
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
this.repository.save(toSave);
JdbcOperationsSessionRepository.JdbcSession getSession = this.repository.findById(toSave.getId());
JdbcSession getSession = this.repository.findById(toSave.getId());
getSession.setAttribute(INDEX_NAME, null);
this.repository.save(getSession);
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName = this.repository
.findByIndexNameAndIndexValue(INDEX_NAME, getChangedSecurityName());
Map<String, JdbcSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
getChangedSecurityName());
assertThat(findByPrincipalName).isEmpty();
}
@Test
void findByChangedSecurityPrincipalNameReload() {
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
JdbcSession toSave = this.repository.createSession();
toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context);
this.repository.save(toSave);
JdbcOperationsSessionRepository.JdbcSession getSession = this.repository.findById(toSave.getId());
JdbcSession getSession = this.repository.findById(toSave.getId());
getSession.setAttribute(SPRING_SECURITY_CONTEXT, this.changedContext);
this.repository.save(getSession);
Map<String, JdbcOperationsSessionRepository.JdbcSession> findByPrincipalName = this.repository
.findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName());
Map<String, JdbcSession> findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME,
getSecurityName());
assertThat(findByPrincipalName).isEmpty();
findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, getChangedSecurityName());
@@ -509,7 +510,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
@Test
void cleanupInactiveSessionsUsingRepositoryDefinedInterval() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
JdbcSession session = this.repository.createSession();
this.repository.save(session);
@@ -537,7 +538,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
// gh-580
@Test
void cleanupInactiveSessionsUsingSessionDefinedInterval() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
JdbcSession session = this.repository.createSession();
session.setMaxInactiveInterval(Duration.ofMinutes(45));
this.repository.save(session);
@@ -567,12 +568,12 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
void changeSessionIdWhenOnlyChangeId() {
String attrName = "changeSessionId";
String attrValue = "changeSessionId-value";
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
JdbcSession toSave = this.repository.createSession();
toSave.setAttribute(attrName, attrValue);
this.repository.save(toSave);
JdbcOperationsSessionRepository.JdbcSession findById = this.repository.findById(toSave.getId());
JdbcSession findById = this.repository.findById(toSave.getId());
assertThat(findById.<String>getAttribute(attrName)).isEqualTo(attrValue);
@@ -583,7 +584,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
assertThat(this.repository.findById(originalFindById)).isNull();
JdbcOperationsSessionRepository.JdbcSession findByChangeSessionId = this.repository.findById(changeSessionId);
JdbcSession findByChangeSessionId = this.repository.findById(changeSessionId);
assertThat(findByChangeSessionId.isChanged()).isFalse();
assertThat(findByChangeSessionId.getDelta()).isEmpty();
@@ -592,7 +593,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
@Test
void changeSessionIdWhenChangeTwice() {
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
JdbcSession toSave = this.repository.createSession();
this.repository.save(toSave);
@@ -612,11 +613,11 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
String attrName = "changeSessionId";
String attrValue = "changeSessionId-value";
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
JdbcSession toSave = this.repository.createSession();
this.repository.save(toSave);
JdbcOperationsSessionRepository.JdbcSession findById = this.repository.findById(toSave.getId());
JdbcSession findById = this.repository.findById(toSave.getId());
findById.setAttribute(attrName, attrValue);
@@ -627,7 +628,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
assertThat(this.repository.findById(originalFindById)).isNull();
JdbcOperationsSessionRepository.JdbcSession findByChangeSessionId = this.repository.findById(changeSessionId);
JdbcSession findByChangeSessionId = this.repository.findById(changeSessionId);
assertThat(findByChangeSessionId.isChanged()).isFalse();
assertThat(findByChangeSessionId.getDelta()).isEmpty();
@@ -636,7 +637,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
@Test
void changeSessionIdWhenHasNotSaved() {
JdbcOperationsSessionRepository.JdbcSession toSave = this.repository.createSession();
JdbcSession toSave = this.repository.createSession();
String originalId = toSave.getId();
toSave.changeSessionId();
@@ -648,7 +649,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
@Test // gh-1070
void saveUpdatedAddAndModifyAttribute() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
JdbcSession session = this.repository.createSession();
this.repository.save(session);
session = this.repository.findById(session.getId());
session.setAttribute("testName", "testValue1");
@@ -661,7 +662,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
@Test // gh-1070
void saveUpdatedAddAndRemoveAttribute() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
JdbcSession session = this.repository.createSession();
this.repository.save(session);
session = this.repository.findById(session.getId());
session.setAttribute("testName", "testValue");
@@ -674,7 +675,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
@Test // gh-1070
void saveUpdatedModifyAndRemoveAttribute() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
JdbcSession session = this.repository.createSession();
session.setAttribute("testName", "testValue1");
this.repository.save(session);
session = this.repository.findById(session.getId());
@@ -688,7 +689,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
@Test // gh-1070
void saveUpdatedRemoveAndAddAttribute() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
JdbcSession session = this.repository.createSession();
session.setAttribute("testName", "testValue1");
this.repository.save(session);
session = this.repository.findById(session.getId());
@@ -702,7 +703,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
@Test // gh-1031
void saveDeleted() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
JdbcSession session = this.repository.createSession();
this.repository.save(session);
session = this.repository.findById(session.getId());
this.repository.deleteById(session.getId());
@@ -714,7 +715,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
@Test // gh-1031
void saveDeletedAddAttribute() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
JdbcSession session = this.repository.createSession();
this.repository.save(session);
session = this.repository.findById(session.getId());
this.repository.deleteById(session.getId());
@@ -727,7 +728,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
@Test // gh-1133
void sessionFromStoreResolvesAttributesLazily() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
JdbcSession session = this.repository.createSession();
session.setAttribute("attribute1", "value1");
session.setAttribute("attribute2", "value2");
this.repository.save(session);
@@ -749,7 +750,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
String attributeName = "largeAttribute";
int arraySize = 4000;
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
JdbcSession session = this.repository.createSession();
session.setAttribute(attributeName, new byte[arraySize]);
this.repository.save(session);
session = this.repository.findById(session.getId());
@@ -770,7 +771,7 @@ abstract class AbstractJdbcOperationsSessionRepositoryITests {
static class BaseConfig {
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}

View File

@@ -16,6 +16,9 @@
package org.springframework.session.jdbc;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import org.testcontainers.containers.Db2Container;
import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.containers.MSSQLServerContainer;
@@ -23,6 +26,7 @@ import org.testcontainers.containers.MariaDBContainer;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.containers.OracleContainer;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
/**
* Factories for various {@link JdbcDatabaseContainer}s.
@@ -54,8 +58,8 @@ final class DatabaseContainers {
return new MySql8Container();
}
static OracleContainer oracle() {
return new OracleContainer();
static OracleXeContainer oracleXe() {
return new OracleXeContainer();
}
static PostgreSQLContainer postgreSql9() {
@@ -100,7 +104,7 @@ final class DatabaseContainers {
private static class MariaDb10Container extends MariaDBContainer<MariaDb10Container> {
MariaDb10Container() {
super("mariadb:10.4.7");
super("mariadb:10.4.8");
}
@Override
@@ -149,10 +153,28 @@ final class DatabaseContainers {
}
private static class OracleXeContainer extends OracleContainer {
@Override
protected void configure() {
super.configure();
this.waitStrategy = new LogMessageWaitStrategy().withRegEx(".*DATABASE IS READY TO USE!.*\\s")
.withStartupTimeout(Duration.of(10, ChronoUnit.MINUTES));
setShmSize(1024L * 1024L * 1024L);
addEnv("ORACLE_PWD", getPassword());
}
@Override
protected void waitUntilContainerStarted() {
getWaitStrategy().waitUntilReady(this);
}
}
private static class PostgreSql9Container extends PostgreSQLContainer<PostgreSql9Container> {
PostgreSql9Container() {
super("postgres:9.6.14");
super("postgres:9.6.15");
}
}
@@ -160,7 +182,7 @@ final class DatabaseContainers {
private static class PostgreSql10Container extends PostgreSQLContainer<PostgreSql10Container> {
PostgreSql10Container() {
super("postgres:10.9");
super("postgres:10.10");
}
}
@@ -168,7 +190,7 @@ final class DatabaseContainers {
private static class PostgreSql11Container extends PostgreSQLContainer<PostgreSql11Container> {
PostgreSql11Container() {
super("postgres:11.4");
super("postgres:11.5");
}
}
@@ -176,7 +198,7 @@ final class DatabaseContainers {
private static class SqlServer2017Container extends MSSQLServerContainer<SqlServer2017Container> {
SqlServer2017Container() {
super("mcr.microsoft.com/mssql/server:2017-cu16");
super("mcr.microsoft.com/mssql/server:2017-CU16");
}
}

View File

@@ -27,28 +27,27 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
/**
* Integration tests for {@link JdbcOperationsSessionRepository} using IBM DB2 11.x
* database.
* Integration tests for {@link JdbcIndexedSessionRepository} using IBM DB2 11.x database.
*
* @author Vedran Pavic
*/
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration
class Db211JdbcOperationsSessionRepositoryITests extends AbstractContainerJdbcOperationsSessionRepositoryITests {
class Db211JdbcIndexedSessionRepositoryITests extends AbstractContainerJdbcIndexedSessionRepositoryITests {
@Configuration
static class Config extends BaseContainerConfig {
@Bean
public Db2Container databaseContainer() {
Db2Container databaseContainer() {
Db2Container databaseContainer = DatabaseContainers.db211();
databaseContainer.start();
return databaseContainer;
}
@Bean
public ResourceDatabasePopulator databasePopulator() {
ResourceDatabasePopulator databasePopulator() {
return DatabasePopulators.db2();
}

View File

@@ -28,20 +28,20 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
/**
* Integration tests for {@link JdbcOperationsSessionRepository} using Derby database.
* Integration tests for {@link JdbcIndexedSessionRepository} using Derby database.
*
* @author Vedran Pavic
*/
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration
class DerbyJdbcOperationsSessionRepositoryITests extends AbstractJdbcOperationsSessionRepositoryITests {
class DerbyJdbcIndexedSessionRepositoryITests extends AbstractJdbcIndexedSessionRepositoryITests {
@Configuration
static class Config extends BaseConfig {
@Bean
public EmbeddedDatabase dataSource() {
EmbeddedDatabase dataSource() {
return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.DERBY)
.addScript("org/springframework/session/jdbc/schema-derby.sql").build();
}

View File

@@ -28,20 +28,20 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
/**
* Integration tests for {@link JdbcOperationsSessionRepository} using H2 database.
* Integration tests for {@link JdbcIndexedSessionRepository} using H2 database.
*
* @author Vedran Pavic
*/
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration
class H2JdbcOperationsSessionRepositoryITests extends AbstractJdbcOperationsSessionRepositoryITests {
class H2JdbcIndexedSessionRepositoryITests extends AbstractJdbcIndexedSessionRepositoryITests {
@Configuration
static class Config extends BaseConfig {
@Bean
public EmbeddedDatabase dataSource() {
EmbeddedDatabase dataSource() {
return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2)
.addScript("org/springframework/session/jdbc/schema-h2.sql").build();
}

View File

@@ -28,20 +28,20 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
/**
* Integration tests for {@link JdbcOperationsSessionRepository} using HSQLDB database.
* Integration tests for {@link JdbcIndexedSessionRepository} using HSQLDB database.
*
* @author Vedran Pavic
*/
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration
class HsqldbJdbcOperationsSessionRepositoryITests extends AbstractJdbcOperationsSessionRepositoryITests {
class HsqldbJdbcIndexedSessionRepositoryITests extends AbstractJdbcIndexedSessionRepositoryITests {
@Configuration
static class Config extends BaseConfig {
@Bean
public EmbeddedDatabase dataSource() {
EmbeddedDatabase dataSource() {
return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.HSQL)
.addScript("org/springframework/session/jdbc/schema-hsqldb.sql").build();
}

View File

@@ -27,28 +27,27 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
/**
* Integration tests for {@link JdbcOperationsSessionRepository} using MariaDB 10.x
* database.
* Integration tests for {@link JdbcIndexedSessionRepository} using MariaDB 10.x database.
*
* @author Vedran Pavic
*/
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration
class MariaDb10JdbcOperationsSessionRepositoryITests extends AbstractContainerJdbcOperationsSessionRepositoryITests {
class MariaDb10JdbcIndexedSessionRepositoryITests extends AbstractContainerJdbcIndexedSessionRepositoryITests {
@Configuration
static class Config extends BaseContainerConfig {
@Bean
public MariaDBContainer databaseContainer() {
MariaDBContainer databaseContainer() {
MariaDBContainer databaseContainer = DatabaseContainers.mariaDb10();
databaseContainer.start();
return databaseContainer;
}
@Bean
public ResourceDatabasePopulator databasePopulator() {
ResourceDatabasePopulator databasePopulator() {
return DatabasePopulators.mySql();
}

View File

@@ -27,28 +27,27 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
/**
* Integration tests for {@link JdbcOperationsSessionRepository} using MariaDB 5.x
* database.
* Integration tests for {@link JdbcIndexedSessionRepository} using MariaDB 5.x database.
*
* @author Vedran Pavic
*/
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration
class MariaDb5JdbcOperationsSessionRepositoryITests extends AbstractContainerJdbcOperationsSessionRepositoryITests {
class MariaDb5JdbcIndexedSessionRepositoryITests extends AbstractContainerJdbcIndexedSessionRepositoryITests {
@Configuration
static class Config extends BaseContainerConfig {
@Bean
public MariaDBContainer databaseContainer() {
MariaDBContainer databaseContainer() {
MariaDBContainer databaseContainer = DatabaseContainers.mariaDb5();
databaseContainer.start();
return databaseContainer;
}
@Bean
public ResourceDatabasePopulator databasePopulator() {
ResourceDatabasePopulator databasePopulator() {
return DatabasePopulators.mySql();
}

View File

@@ -27,27 +27,27 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
/**
* Integration tests for {@link JdbcOperationsSessionRepository} using MySQL 5.x database.
* Integration tests for {@link JdbcIndexedSessionRepository} using MySQL 5.x database.
*
* @author Vedran Pavic
*/
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration
class MySql5JdbcOperationsSessionRepositoryITests extends AbstractContainerJdbcOperationsSessionRepositoryITests {
class MySql5JdbcIndexedSessionRepositoryITests extends AbstractContainerJdbcIndexedSessionRepositoryITests {
@Configuration
static class Config extends BaseContainerConfig {
@Bean
public MySQLContainer databaseContainer() {
MySQLContainer databaseContainer() {
MySQLContainer databaseContainer = DatabaseContainers.mySql5();
databaseContainer.start();
return databaseContainer;
}
@Bean
public ResourceDatabasePopulator databasePopulator() {
ResourceDatabasePopulator databasePopulator() {
return DatabasePopulators.mySql();
}

View File

@@ -27,27 +27,27 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
/**
* Integration tests for {@link JdbcOperationsSessionRepository} using MySQL 8.x database.
* Integration tests for {@link JdbcIndexedSessionRepository} using MySQL 8.x database.
*
* @author Vedran Pavic
*/
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration
class MySql8JdbcOperationsSessionRepositoryITests extends AbstractContainerJdbcOperationsSessionRepositoryITests {
class MySql8JdbcIndexedSessionRepositoryITests extends AbstractContainerJdbcIndexedSessionRepositoryITests {
@Configuration
static class Config extends BaseContainerConfig {
@Bean
public MySQLContainer databaseContainer() {
MySQLContainer databaseContainer() {
MySQLContainer databaseContainer = DatabaseContainers.mySql8();
databaseContainer.start();
return databaseContainer;
}
@Bean
public ResourceDatabasePopulator databasePopulator() {
ResourceDatabasePopulator databasePopulator() {
return DatabasePopulators.mySql();
}

View File

@@ -28,27 +28,24 @@ import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.util.ClassUtils;
/**
* Integration tests for {@link JdbcOperationsSessionRepository} using Oracle database.
* Integration tests for {@link JdbcIndexedSessionRepository} using Oracle database.
* <p>
* This test is conditional on presence of Oracle JDBC driver on the classpath and
* Testcontainers property {@code oracle.container.image} being set.
* This test is conditional on Testcontainers property {@code oracle.container.image}
* being set.
*
* @author Vedran Pavic
*/
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration
class OracleJdbcOperationsSessionRepositoryITests extends AbstractContainerJdbcOperationsSessionRepositoryITests {
class OracleJdbcIndexedSessionRepositoryITests extends AbstractContainerJdbcIndexedSessionRepositoryITests {
@BeforeAll
static void setUpClass() {
Assumptions.assumeTrue(ClassUtils.isPresent("oracle.jdbc.OracleDriver", null),
"Oracle JDBC driver is present on the classpath");
Assumptions.assumeTrue(
TestcontainersConfiguration.getInstance().getProperties().getProperty("oracle.container.image") != null,
TestcontainersConfiguration.getInstance().getProperties().containsKey("oracle.container.image"),
"Testcontainers property `oracle.container.image` is set");
}
@@ -56,14 +53,14 @@ class OracleJdbcOperationsSessionRepositoryITests extends AbstractContainerJdbcO
static class Config extends BaseContainerConfig {
@Bean
public OracleContainer databaseContainer() {
OracleContainer databaseContainer = DatabaseContainers.oracle();
OracleContainer databaseContainer() {
OracleContainer databaseContainer = DatabaseContainers.oracleXe();
databaseContainer.start();
return databaseContainer;
}
@Bean
public ResourceDatabasePopulator databasePopulator() {
ResourceDatabasePopulator databasePopulator() {
return DatabasePopulators.oracle();
}

View File

@@ -27,7 +27,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
/**
* Integration tests for {@link JdbcOperationsSessionRepository} using PostgreSQL 10.x
* Integration tests for {@link JdbcIndexedSessionRepository} using PostgreSQL 10.x
* database.
*
* @author Vedran Pavic
@@ -35,20 +35,20 @@ import org.springframework.test.context.web.WebAppConfiguration;
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration
class PostgreSql10JdbcOperationsSessionRepositoryITests extends AbstractContainerJdbcOperationsSessionRepositoryITests {
class PostgreSql10JdbcIndexedSessionRepositoryITests extends AbstractContainerJdbcIndexedSessionRepositoryITests {
@Configuration
static class Config extends BaseContainerConfig {
@Bean
public PostgreSQLContainer databaseContainer() {
PostgreSQLContainer databaseContainer() {
PostgreSQLContainer databaseContainer = DatabaseContainers.postgreSql10();
databaseContainer.start();
return databaseContainer;
}
@Bean
public ResourceDatabasePopulator databasePopulator() {
ResourceDatabasePopulator databasePopulator() {
return DatabasePopulators.postgreSql();
}

View File

@@ -27,7 +27,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
/**
* Integration tests for {@link JdbcOperationsSessionRepository} using PostgreSQL 11.x
* Integration tests for {@link JdbcIndexedSessionRepository} using PostgreSQL 11.x
* database.
*
* @author Vedran Pavic
@@ -35,20 +35,20 @@ import org.springframework.test.context.web.WebAppConfiguration;
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration
class PostgreSql11JdbcOperationsSessionRepositoryITests extends AbstractContainerJdbcOperationsSessionRepositoryITests {
class PostgreSql11JdbcIndexedSessionRepositoryITests extends AbstractContainerJdbcIndexedSessionRepositoryITests {
@Configuration
static class Config extends BaseContainerConfig {
@Bean
public PostgreSQLContainer databaseContainer() {
PostgreSQLContainer databaseContainer() {
PostgreSQLContainer databaseContainer = DatabaseContainers.postgreSql11();
databaseContainer.start();
return databaseContainer;
}
@Bean
public ResourceDatabasePopulator databasePopulator() {
ResourceDatabasePopulator databasePopulator() {
return DatabasePopulators.postgreSql();
}

View File

@@ -27,7 +27,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
/**
* Integration tests for {@link JdbcOperationsSessionRepository} using PostgreSQL 9.x
* Integration tests for {@link JdbcIndexedSessionRepository} using PostgreSQL 9.x
* database.
*
* @author Vedran Pavic
@@ -35,20 +35,20 @@ import org.springframework.test.context.web.WebAppConfiguration;
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration
class PostgreSql9JdbcOperationsSessionRepositoryITests extends AbstractContainerJdbcOperationsSessionRepositoryITests {
class PostgreSql9JdbcIndexedSessionRepositoryITests extends AbstractContainerJdbcIndexedSessionRepositoryITests {
@Configuration
static class Config extends BaseContainerConfig {
@Bean
public PostgreSQLContainer databaseContainer() {
PostgreSQLContainer databaseContainer() {
PostgreSQLContainer databaseContainer = DatabaseContainers.postgreSql9();
databaseContainer.start();
return databaseContainer;
}
@Bean
public ResourceDatabasePopulator databasePopulator() {
ResourceDatabasePopulator databasePopulator() {
return DatabasePopulators.postgreSql();
}

View File

@@ -27,28 +27,28 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
/**
* Integration tests for {@link JdbcOperationsSessionRepository} using Microsoft SQL
* Server 2017 database.
* Integration tests for {@link JdbcIndexedSessionRepository} using Microsoft SQL Server
* 2017 database.
*
* @author Vedran Pavic
*/
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration
class SqlServerJdbcOperationsSessionRepositoryITests extends AbstractContainerJdbcOperationsSessionRepositoryITests {
class SqlServerJdbcIndexedSessionRepositoryITests extends AbstractContainerJdbcIndexedSessionRepositoryITests {
@Configuration
static class Config extends BaseContainerConfig {
@Bean
public MSSQLServerContainer databaseContainer() {
MSSQLServerContainer databaseContainer() {
MSSQLServerContainer databaseContainer = DatabaseContainers.sqlServer2017();
databaseContainer.start();
return databaseContainer;
}
@Bean
public ResourceDatabasePopulator databasePopulator() {
ResourceDatabasePopulator databasePopulator() {
return DatabasePopulators.sqlServer();
}

View File

@@ -1,2 +1,2 @@
ibmcom/db2:11.5.0.0a
mcr.microsoft.com/mssql/server:2017-cu16
mcr.microsoft.com/mssql/server:2017-CU16

View File

@@ -0,0 +1,873 @@
/*
* Copyright 2014-2019 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
*
* https://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.jdbc;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.core.serializer.support.DeserializingConverter;
import org.springframework.core.serializer.support.SerializingConverter;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.support.lob.DefaultLobHandler;
import org.springframework.jdbc.support.lob.LobHandler;
import org.springframework.session.DelegatingIndexResolver;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.FlushMode;
import org.springframework.session.IndexResolver;
import org.springframework.session.MapSession;
import org.springframework.session.PrincipalNameIndexResolver;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.transaction.support.TransactionOperations;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* A {@link org.springframework.session.SessionRepository} implementation that uses
* Spring's {@link JdbcOperations} to store sessions in a relational database. This
* implementation does not support publishing of session events.
* <p>
* An example of how to create a new instance can be seen below:
*
* <pre class="code">
* JdbcTemplate jdbcTemplate = new JdbcTemplate();
*
* // ... configure jdbcTemplate ...
*
* TransactionTemplate transactionTemplate = new TransactionTemplate();
*
* // ... configure transactionTemplate ...
*
* JdbcIndexedSessionRepository sessionRepository =
* new JdbcIndexedSessionRepository(jdbcTemplate, transactionTemplate);
* </pre>
*
* For additional information on how to create and configure {@code JdbcTemplate} and
* {@code TransactionTemplate}, refer to the <a href=
* "https://docs.spring.io/spring/docs/current/spring-framework-reference/html/spring-data-tier.html">
* Spring Framework Reference Documentation</a>.
* <p>
* By default, this implementation uses <code>SPRING_SESSION</code> and
* <code>SPRING_SESSION_ATTRIBUTES</code> tables to store sessions. Note that the table
* name can be customized using the {@link #setTableName(String)} method. In that case the
* table used to store attributes will be named using the provided table name, suffixed
* with <code>_ATTRIBUTES</code>.
*
* Depending on your database, the table definition can be described as below:
*
* <pre class="code">
* CREATE TABLE SPRING_SESSION (
* PRIMARY_ID CHAR(36) NOT NULL,
* SESSION_ID CHAR(36) NOT NULL,
* CREATION_TIME BIGINT NOT NULL,
* LAST_ACCESS_TIME BIGINT NOT NULL,
* MAX_INACTIVE_INTERVAL INT NOT NULL,
* EXPIRY_TIME BIGINT NOT NULL,
* PRINCIPAL_NAME VARCHAR(100),
* CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (PRIMARY_ID)
* );
*
* CREATE UNIQUE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (SESSION_ID);
* CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (EXPIRY_TIME);
* CREATE INDEX SPRING_SESSION_IX3 ON SPRING_SESSION (PRINCIPAL_NAME);
*
* CREATE TABLE SPRING_SESSION_ATTRIBUTES (
* SESSION_PRIMARY_ID CHAR(36) NOT NULL,
* ATTRIBUTE_NAME VARCHAR(200) NOT NULL,
* ATTRIBUTE_BYTES BYTEA NOT NULL,
* CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_PRIMARY_ID, ATTRIBUTE_NAME),
* CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_PRIMARY_ID) REFERENCES SPRING_SESSION(PRIMARY_ID) ON DELETE CASCADE
* );
*
* CREATE INDEX SPRING_SESSION_ATTRIBUTES_IX1 ON SPRING_SESSION_ATTRIBUTES (SESSION_PRIMARY_ID);
* </pre>
*
* Due to the differences between the various database vendors, especially when it comes
* to storing binary data, make sure to use SQL script specific to your database. Scripts
* for most major database vendors are packaged as
* <code>org/springframework/session/jdbc/schema-*.sql</code>, where <code>*</code> is the
* target database type.
*
* @author Vedran Pavic
* @author Craig Andrews
* @since 2.2.0
*/
public class JdbcIndexedSessionRepository
implements FindByIndexNameSessionRepository<JdbcIndexedSessionRepository.JdbcSession> {
/**
* The default name of database table used by Spring Session to store sessions.
*/
public static final String DEFAULT_TABLE_NAME = "SPRING_SESSION";
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
// @formatter:off
private static final String CREATE_SESSION_QUERY = "INSERT INTO %TABLE_NAME%(PRIMARY_ID, SESSION_ID, CREATION_TIME, LAST_ACCESS_TIME, MAX_INACTIVE_INTERVAL, EXPIRY_TIME, PRINCIPAL_NAME) "
+ "VALUES (?, ?, ?, ?, ?, ?, ?)";
// @formatter:on
// @formatter:off
private static final String CREATE_SESSION_ATTRIBUTE_QUERY = "INSERT INTO %TABLE_NAME%_ATTRIBUTES(SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES) "
+ "SELECT PRIMARY_ID, ?, ? "
+ "FROM %TABLE_NAME% "
+ "WHERE SESSION_ID = ?";
// @formatter:on
// @formatter:off
private static final String GET_SESSION_QUERY = "SELECT S.PRIMARY_ID, S.SESSION_ID, S.CREATION_TIME, S.LAST_ACCESS_TIME, S.MAX_INACTIVE_INTERVAL, SA.ATTRIBUTE_NAME, SA.ATTRIBUTE_BYTES "
+ "FROM %TABLE_NAME% S "
+ "LEFT OUTER JOIN %TABLE_NAME%_ATTRIBUTES SA ON S.PRIMARY_ID = SA.SESSION_PRIMARY_ID "
+ "WHERE S.SESSION_ID = ?";
// @formatter:on
// @formatter:off
private static final String UPDATE_SESSION_QUERY = "UPDATE %TABLE_NAME% SET SESSION_ID = ?, LAST_ACCESS_TIME = ?, MAX_INACTIVE_INTERVAL = ?, EXPIRY_TIME = ?, PRINCIPAL_NAME = ? "
+ "WHERE PRIMARY_ID = ?";
// @formatter:on
// @formatter:off
private static final String UPDATE_SESSION_ATTRIBUTE_QUERY = "UPDATE %TABLE_NAME%_ATTRIBUTES SET ATTRIBUTE_BYTES = ? "
+ "WHERE SESSION_PRIMARY_ID = ? "
+ "AND ATTRIBUTE_NAME = ?";
// @formatter:on
// @formatter:off
private static final String DELETE_SESSION_ATTRIBUTE_QUERY = "DELETE FROM %TABLE_NAME%_ATTRIBUTES "
+ "WHERE SESSION_PRIMARY_ID = ? "
+ "AND ATTRIBUTE_NAME = ?";
// @formatter:on
// @formatter:off
private static final String DELETE_SESSION_QUERY = "DELETE FROM %TABLE_NAME% "
+ "WHERE SESSION_ID = ?";
// @formatter:on
// @formatter:off
private static final String LIST_SESSIONS_BY_PRINCIPAL_NAME_QUERY = "SELECT S.PRIMARY_ID, S.SESSION_ID, S.CREATION_TIME, S.LAST_ACCESS_TIME, S.MAX_INACTIVE_INTERVAL, SA.ATTRIBUTE_NAME, SA.ATTRIBUTE_BYTES "
+ "FROM %TABLE_NAME% S "
+ "LEFT OUTER JOIN %TABLE_NAME%_ATTRIBUTES SA ON S.PRIMARY_ID = SA.SESSION_PRIMARY_ID "
+ "WHERE S.PRINCIPAL_NAME = ?";
// @formatter:on
// @formatter:off
private static final String DELETE_SESSIONS_BY_EXPIRY_TIME_QUERY = "DELETE FROM %TABLE_NAME% "
+ "WHERE EXPIRY_TIME < ?";
// @formatter:on
private static final Log logger = LogFactory.getLog(JdbcIndexedSessionRepository.class);
private final JdbcOperations jdbcOperations;
private final TransactionOperations transactionOperations;
private final ResultSetExtractor<List<JdbcSession>> extractor = new SessionResultSetExtractor();
/**
* The name of database table used by Spring Session to store sessions.
*/
private String tableName = DEFAULT_TABLE_NAME;
private String createSessionQuery;
private String createSessionAttributeQuery;
private String getSessionQuery;
private String updateSessionQuery;
private String updateSessionAttributeQuery;
private String deleteSessionAttributeQuery;
private String deleteSessionQuery;
private String listSessionsByPrincipalNameQuery;
private String deleteSessionsByExpiryTimeQuery;
/**
* If non-null, this value is used to override the default value for
* {@link JdbcSession#setMaxInactiveInterval(Duration)}.
*/
private Integer defaultMaxInactiveInterval;
private IndexResolver<Session> indexResolver = new DelegatingIndexResolver<>(new PrincipalNameIndexResolver<>());
private ConversionService conversionService = createDefaultConversionService();
private LobHandler lobHandler = new DefaultLobHandler();
private FlushMode flushMode = FlushMode.ON_SAVE;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
/**
* Create a new {@link JdbcIndexedSessionRepository} instance which uses the provided
* {@link JdbcOperations} and {@link TransactionOperations} to manage sessions.
* @param jdbcOperations the {@link JdbcOperations} to use
* @param transactionOperations the {@link TransactionOperations} to use
*/
public JdbcIndexedSessionRepository(JdbcOperations jdbcOperations, TransactionOperations transactionOperations) {
Assert.notNull(jdbcOperations, "jdbcOperations must not be null");
Assert.notNull(transactionOperations, "transactionOperations must not be null");
this.jdbcOperations = jdbcOperations;
this.transactionOperations = transactionOperations;
prepareQueries();
}
/**
* Set the name of database table used to store sessions.
* @param tableName the database table name
*/
public void setTableName(String tableName) {
Assert.hasText(tableName, "Table name must not be empty");
this.tableName = tableName.trim();
prepareQueries();
}
/**
* Set the custom SQL query used to create the session.
* @param createSessionQuery the SQL query string
*/
public void setCreateSessionQuery(String createSessionQuery) {
Assert.hasText(createSessionQuery, "Query must not be empty");
this.createSessionQuery = createSessionQuery;
}
/**
* Set the custom SQL query used to create the session attribute.
* @param createSessionAttributeQuery the SQL query string
*/
public void setCreateSessionAttributeQuery(String createSessionAttributeQuery) {
Assert.hasText(createSessionAttributeQuery, "Query must not be empty");
this.createSessionAttributeQuery = createSessionAttributeQuery;
}
/**
* Set the custom SQL query used to retrieve the session.
* @param getSessionQuery the SQL query string
*/
public void setGetSessionQuery(String getSessionQuery) {
Assert.hasText(getSessionQuery, "Query must not be empty");
this.getSessionQuery = getSessionQuery;
}
/**
* Set the custom SQL query used to update the session.
* @param updateSessionQuery the SQL query string
*/
public void setUpdateSessionQuery(String updateSessionQuery) {
Assert.hasText(updateSessionQuery, "Query must not be empty");
this.updateSessionQuery = updateSessionQuery;
}
/**
* Set the custom SQL query used to update the session attribute.
* @param updateSessionAttributeQuery the SQL query string
*/
public void setUpdateSessionAttributeQuery(String updateSessionAttributeQuery) {
Assert.hasText(updateSessionAttributeQuery, "Query must not be empty");
this.updateSessionAttributeQuery = updateSessionAttributeQuery;
}
/**
* Set the custom SQL query used to delete the session attribute.
* @param deleteSessionAttributeQuery the SQL query string
*/
public void setDeleteSessionAttributeQuery(String deleteSessionAttributeQuery) {
Assert.hasText(deleteSessionAttributeQuery, "Query must not be empty");
this.deleteSessionAttributeQuery = deleteSessionAttributeQuery;
}
/**
* Set the custom SQL query used to delete the session.
* @param deleteSessionQuery the SQL query string
*/
public void setDeleteSessionQuery(String deleteSessionQuery) {
Assert.hasText(deleteSessionQuery, "Query must not be empty");
this.deleteSessionQuery = deleteSessionQuery;
}
/**
* Set the custom SQL query used to retrieve the sessions by principal name.
* @param listSessionsByPrincipalNameQuery the SQL query string
*/
public void setListSessionsByPrincipalNameQuery(String listSessionsByPrincipalNameQuery) {
Assert.hasText(listSessionsByPrincipalNameQuery, "Query must not be empty");
this.listSessionsByPrincipalNameQuery = listSessionsByPrincipalNameQuery;
}
/**
* Set the custom SQL query used to delete the sessions by last access time.
* @param deleteSessionsByExpiryTimeQuery the SQL query string
*/
public void setDeleteSessionsByExpiryTimeQuery(String deleteSessionsByExpiryTimeQuery) {
Assert.hasText(deleteSessionsByExpiryTimeQuery, "Query must not be empty");
this.deleteSessionsByExpiryTimeQuery = deleteSessionsByExpiryTimeQuery;
}
/**
* 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;
}
/**
* Set the {@link IndexResolver} to use.
* @param indexResolver the index resolver
*/
public void setIndexResolver(IndexResolver<Session> indexResolver) {
Assert.notNull(indexResolver, "indexResolver cannot be null");
this.indexResolver = indexResolver;
}
public void setLobHandler(LobHandler lobHandler) {
Assert.notNull(lobHandler, "LobHandler must not be null");
this.lobHandler = lobHandler;
}
/**
* Sets the {@link ConversionService} to use.
* @param conversionService the converter to set
*/
public void setConversionService(ConversionService conversionService) {
Assert.notNull(conversionService, "conversionService must not be null");
this.conversionService = conversionService;
}
/**
* Set the flush mode. Default is {@link FlushMode#ON_SAVE}.
* @param flushMode the flush mode
*/
public void setFlushMode(FlushMode flushMode) {
Assert.notNull(flushMode, "flushMode must not be null");
this.flushMode = flushMode;
}
/**
* Set the save mode.
* @param saveMode the save mode
*/
public void setSaveMode(SaveMode saveMode) {
Assert.notNull(saveMode, "saveMode must not be null");
this.saveMode = saveMode;
}
@Override
public JdbcSession createSession() {
MapSession delegate = new MapSession();
if (this.defaultMaxInactiveInterval != null) {
delegate.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
}
JdbcSession session = new JdbcSession(delegate, UUID.randomUUID().toString(), true);
session.flushIfRequired();
return session;
}
@Override
public void save(final JdbcSession session) {
session.save();
}
@Override
public JdbcSession findById(final String id) {
final JdbcSession session = this.transactionOperations.execute((status) -> {
List<JdbcSession> sessions = JdbcIndexedSessionRepository.this.jdbcOperations.query(
JdbcIndexedSessionRepository.this.getSessionQuery, (ps) -> ps.setString(1, id),
JdbcIndexedSessionRepository.this.extractor);
if (sessions.isEmpty()) {
return null;
}
return sessions.get(0);
});
if (session != null) {
if (session.isExpired()) {
deleteById(id);
}
else {
return session;
}
}
return null;
}
@Override
public void deleteById(final String id) {
this.transactionOperations.executeWithoutResult((status) -> JdbcIndexedSessionRepository.this.jdbcOperations
.update(JdbcIndexedSessionRepository.this.deleteSessionQuery, id));
}
@Override
public Map<String, JdbcSession> findByIndexNameAndIndexValue(String indexName, final String indexValue) {
if (!PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) {
return Collections.emptyMap();
}
List<JdbcSession> sessions = this.transactionOperations
.execute((status) -> JdbcIndexedSessionRepository.this.jdbcOperations.query(
JdbcIndexedSessionRepository.this.listSessionsByPrincipalNameQuery,
(ps) -> ps.setString(1, indexValue), JdbcIndexedSessionRepository.this.extractor));
Map<String, JdbcSession> sessionMap = new HashMap<>(sessions.size());
for (JdbcSession session : sessions) {
sessionMap.put(session.getId(), session);
}
return sessionMap;
}
private void insertSessionAttributes(JdbcSession session, List<String> attributeNames) {
Assert.notEmpty(attributeNames, "attributeNames must not be null or empty");
if (attributeNames.size() > 1) {
this.jdbcOperations.batchUpdate(this.createSessionAttributeQuery, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
String attributeName = attributeNames.get(i);
ps.setString(1, attributeName);
getLobHandler().getLobCreator().setBlobAsBytes(ps, 2,
serialize(session.getAttribute(attributeName)));
ps.setString(3, session.getId());
}
@Override
public int getBatchSize() {
return attributeNames.size();
}
});
}
else {
this.jdbcOperations.update(this.createSessionAttributeQuery, (ps) -> {
String attributeName = attributeNames.get(0);
ps.setString(1, attributeName);
getLobHandler().getLobCreator().setBlobAsBytes(ps, 2, serialize(session.getAttribute(attributeName)));
ps.setString(3, session.getId());
});
}
}
private void updateSessionAttributes(JdbcSession session, List<String> attributeNames) {
Assert.notEmpty(attributeNames, "attributeNames must not be null or empty");
if (attributeNames.size() > 1) {
this.jdbcOperations.batchUpdate(this.updateSessionAttributeQuery, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
String attributeName = attributeNames.get(i);
getLobHandler().getLobCreator().setBlobAsBytes(ps, 1,
serialize(session.getAttribute(attributeName)));
ps.setString(2, session.primaryKey);
ps.setString(3, attributeName);
}
@Override
public int getBatchSize() {
return attributeNames.size();
}
});
}
else {
this.jdbcOperations.update(this.updateSessionAttributeQuery, (ps) -> {
String attributeName = attributeNames.get(0);
getLobHandler().getLobCreator().setBlobAsBytes(ps, 1, serialize(session.getAttribute(attributeName)));
ps.setString(2, session.primaryKey);
ps.setString(3, attributeName);
});
}
}
private void deleteSessionAttributes(JdbcSession session, List<String> attributeNames) {
Assert.notEmpty(attributeNames, "attributeNames must not be null or empty");
if (attributeNames.size() > 1) {
this.jdbcOperations.batchUpdate(this.deleteSessionAttributeQuery, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
String attributeName = attributeNames.get(i);
ps.setString(1, session.primaryKey);
ps.setString(2, attributeName);
}
@Override
public int getBatchSize() {
return attributeNames.size();
}
});
}
else {
this.jdbcOperations.update(this.deleteSessionAttributeQuery, (ps) -> {
String attributeName = attributeNames.get(0);
ps.setString(1, session.primaryKey);
ps.setString(2, attributeName);
});
}
}
public void cleanUpExpiredSessions() {
Integer deletedCount = this.transactionOperations
.execute((status) -> JdbcIndexedSessionRepository.this.jdbcOperations.update(
JdbcIndexedSessionRepository.this.deleteSessionsByExpiryTimeQuery, System.currentTimeMillis()));
if (logger.isDebugEnabled()) {
logger.debug("Cleaned up " + deletedCount + " expired sessions");
}
}
private static GenericConversionService createDefaultConversionService() {
GenericConversionService converter = new GenericConversionService();
converter.addConverter(Object.class, byte[].class, new SerializingConverter());
converter.addConverter(byte[].class, Object.class, new DeserializingConverter());
return converter;
}
private String getQuery(String base) {
return StringUtils.replace(base, "%TABLE_NAME%", this.tableName);
}
private void prepareQueries() {
this.createSessionQuery = getQuery(CREATE_SESSION_QUERY);
this.createSessionAttributeQuery = getQuery(CREATE_SESSION_ATTRIBUTE_QUERY);
this.getSessionQuery = getQuery(GET_SESSION_QUERY);
this.updateSessionQuery = getQuery(UPDATE_SESSION_QUERY);
this.updateSessionAttributeQuery = getQuery(UPDATE_SESSION_ATTRIBUTE_QUERY);
this.deleteSessionAttributeQuery = getQuery(DELETE_SESSION_ATTRIBUTE_QUERY);
this.deleteSessionQuery = getQuery(DELETE_SESSION_QUERY);
this.listSessionsByPrincipalNameQuery = getQuery(LIST_SESSIONS_BY_PRINCIPAL_NAME_QUERY);
this.deleteSessionsByExpiryTimeQuery = getQuery(DELETE_SESSIONS_BY_EXPIRY_TIME_QUERY);
}
private LobHandler getLobHandler() {
return this.lobHandler;
}
private byte[] serialize(Object object) {
return (byte[]) this.conversionService.convert(object, TypeDescriptor.valueOf(Object.class),
TypeDescriptor.valueOf(byte[].class));
}
private Object deserialize(byte[] bytes) {
return this.conversionService.convert(bytes, TypeDescriptor.valueOf(byte[].class),
TypeDescriptor.valueOf(Object.class));
}
private enum DeltaValue {
ADDED, UPDATED, REMOVED
}
private static <T> Supplier<T> value(T value) {
return (value != null) ? () -> value : null;
}
private static <T> Supplier<T> lazily(Supplier<T> supplier) {
Supplier<T> lazySupplier = new Supplier<T>() {
private T value;
@Override
public T get() {
if (this.value == null) {
this.value = supplier.get();
}
return this.value;
}
};
return (supplier != null) ? lazySupplier : null;
}
/**
* The {@link Session} to use for {@link JdbcIndexedSessionRepository}.
*
* @author Vedran Pavic
*/
final class JdbcSession implements Session {
private final Session delegate;
private final String primaryKey;
private boolean isNew;
private boolean changed;
private Map<String, DeltaValue> delta = new HashMap<>();
JdbcSession(MapSession delegate, String primaryKey, boolean isNew) {
this.delegate = delegate;
this.primaryKey = primaryKey;
this.isNew = isNew;
if (this.isNew || (JdbcIndexedSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
getAttributeNames().forEach((attributeName) -> this.delta.put(attributeName, DeltaValue.UPDATED));
}
}
boolean isNew() {
return this.isNew;
}
boolean isChanged() {
return this.changed;
}
Map<String, DeltaValue> getDelta() {
return this.delta;
}
void clearChangeFlags() {
this.isNew = false;
this.changed = false;
this.delta.clear();
}
Instant getExpiryTime() {
return getLastAccessedTime().plus(getMaxInactiveInterval());
}
@Override
public String getId() {
return this.delegate.getId();
}
@Override
public String changeSessionId() {
this.changed = true;
return this.delegate.changeSessionId();
}
@Override
public <T> T getAttribute(String attributeName) {
Supplier<T> supplier = this.delegate.getAttribute(attributeName);
if (supplier == null) {
return null;
}
T attributeValue = supplier.get();
if (attributeValue != null
&& JdbcIndexedSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
this.delta.put(attributeName, DeltaValue.UPDATED);
}
return attributeValue;
}
@Override
public Set<String> getAttributeNames() {
return this.delegate.getAttributeNames();
}
@Override
public void setAttribute(String attributeName, Object attributeValue) {
boolean attributeExists = (this.delegate.getAttribute(attributeName) != null);
boolean attributeRemoved = (attributeValue == null);
if (!attributeExists && attributeRemoved) {
return;
}
if (attributeExists) {
if (attributeRemoved) {
this.delta.merge(attributeName, DeltaValue.REMOVED,
(oldDeltaValue, deltaValue) -> (oldDeltaValue == DeltaValue.ADDED) ? null : deltaValue);
}
else {
this.delta.merge(attributeName, DeltaValue.UPDATED, (oldDeltaValue,
deltaValue) -> (oldDeltaValue == DeltaValue.ADDED) ? oldDeltaValue : deltaValue);
}
}
else {
this.delta.merge(attributeName, DeltaValue.ADDED, (oldDeltaValue,
deltaValue) -> (oldDeltaValue == DeltaValue.ADDED) ? oldDeltaValue : DeltaValue.UPDATED);
}
this.delegate.setAttribute(attributeName, value(attributeValue));
if (PRINCIPAL_NAME_INDEX_NAME.equals(attributeName) || SPRING_SECURITY_CONTEXT.equals(attributeName)) {
this.changed = true;
}
flushIfRequired();
}
@Override
public void removeAttribute(String attributeName) {
setAttribute(attributeName, null);
}
@Override
public Instant getCreationTime() {
return this.delegate.getCreationTime();
}
@Override
public void setLastAccessedTime(Instant lastAccessedTime) {
this.delegate.setLastAccessedTime(lastAccessedTime);
this.changed = true;
flushIfRequired();
}
@Override
public Instant getLastAccessedTime() {
return this.delegate.getLastAccessedTime();
}
@Override
public void setMaxInactiveInterval(Duration interval) {
this.delegate.setMaxInactiveInterval(interval);
this.changed = true;
flushIfRequired();
}
@Override
public Duration getMaxInactiveInterval() {
return this.delegate.getMaxInactiveInterval();
}
@Override
public boolean isExpired() {
return this.delegate.isExpired();
}
private void flushIfRequired() {
if (JdbcIndexedSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {
save();
}
}
private void save() {
if (this.isNew) {
JdbcIndexedSessionRepository.this.transactionOperations.executeWithoutResult((status) -> {
Map<String, String> indexes = JdbcIndexedSessionRepository.this.indexResolver
.resolveIndexesFor(JdbcSession.this);
JdbcIndexedSessionRepository.this.jdbcOperations
.update(JdbcIndexedSessionRepository.this.createSessionQuery, (ps) -> {
ps.setString(1, JdbcSession.this.primaryKey);
ps.setString(2, getId());
ps.setLong(3, getCreationTime().toEpochMilli());
ps.setLong(4, getLastAccessedTime().toEpochMilli());
ps.setInt(5, (int) getMaxInactiveInterval().getSeconds());
ps.setLong(6, getExpiryTime().toEpochMilli());
ps.setString(7, indexes.get(PRINCIPAL_NAME_INDEX_NAME));
});
Set<String> attributeNames = getAttributeNames();
if (!attributeNames.isEmpty()) {
insertSessionAttributes(JdbcSession.this, new ArrayList<>(attributeNames));
}
});
}
else {
JdbcIndexedSessionRepository.this.transactionOperations.executeWithoutResult((status) -> {
if (JdbcSession.this.changed) {
Map<String, String> indexes = JdbcIndexedSessionRepository.this.indexResolver
.resolveIndexesFor(JdbcSession.this);
JdbcIndexedSessionRepository.this.jdbcOperations
.update(JdbcIndexedSessionRepository.this.updateSessionQuery, (ps) -> {
ps.setString(1, getId());
ps.setLong(2, getLastAccessedTime().toEpochMilli());
ps.setInt(3, (int) getMaxInactiveInterval().getSeconds());
ps.setLong(4, getExpiryTime().toEpochMilli());
ps.setString(5, indexes.get(PRINCIPAL_NAME_INDEX_NAME));
ps.setString(6, JdbcSession.this.primaryKey);
});
}
List<String> addedAttributeNames = JdbcSession.this.delta.entrySet().stream()
.filter((entry) -> entry.getValue() == DeltaValue.ADDED).map(Map.Entry::getKey)
.collect(Collectors.toList());
if (!addedAttributeNames.isEmpty()) {
insertSessionAttributes(JdbcSession.this, addedAttributeNames);
}
List<String> updatedAttributeNames = JdbcSession.this.delta.entrySet().stream()
.filter((entry) -> entry.getValue() == DeltaValue.UPDATED).map(Map.Entry::getKey)
.collect(Collectors.toList());
if (!updatedAttributeNames.isEmpty()) {
updateSessionAttributes(JdbcSession.this, updatedAttributeNames);
}
List<String> removedAttributeNames = JdbcSession.this.delta.entrySet().stream()
.filter((entry) -> entry.getValue() == DeltaValue.REMOVED).map(Map.Entry::getKey)
.collect(Collectors.toList());
if (!removedAttributeNames.isEmpty()) {
deleteSessionAttributes(JdbcSession.this, removedAttributeNames);
}
});
}
clearChangeFlags();
}
}
private class SessionResultSetExtractor implements ResultSetExtractor<List<JdbcSession>> {
@Override
public List<JdbcSession> extractData(ResultSet rs) throws SQLException, DataAccessException {
List<JdbcSession> sessions = new ArrayList<>();
while (rs.next()) {
String id = rs.getString("SESSION_ID");
JdbcSession session;
if (sessions.size() > 0 && getLast(sessions).getId().equals(id)) {
session = getLast(sessions);
}
else {
MapSession delegate = new MapSession(id);
String primaryKey = rs.getString("PRIMARY_ID");
delegate.setCreationTime(Instant.ofEpochMilli(rs.getLong("CREATION_TIME")));
delegate.setLastAccessedTime(Instant.ofEpochMilli(rs.getLong("LAST_ACCESS_TIME")));
delegate.setMaxInactiveInterval(Duration.ofSeconds(rs.getInt("MAX_INACTIVE_INTERVAL")));
session = new JdbcSession(delegate, primaryKey, false);
}
String attributeName = rs.getString("ATTRIBUTE_NAME");
if (attributeName != null) {
byte[] bytes = getLobHandler().getBlobAsBytes(rs, "ATTRIBUTE_BYTES");
session.delegate.setAttribute(attributeName, lazily(() -> deserialize(bytes)));
}
sessions.add(session);
}
return sessions;
}
private JdbcSession getLast(List<JdbcSession> sessions) {
return sessions.get(sessions.size() - 1);
}
}
}

View File

@@ -16,899 +16,76 @@
package org.springframework.session.jdbc;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.core.serializer.support.DeserializingConverter;
import org.springframework.core.serializer.support.SerializingConverter;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.support.lob.DefaultLobHandler;
import org.springframework.jdbc.support.lob.LobHandler;
import org.springframework.session.DelegatingIndexResolver;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.FlushMode;
import org.springframework.session.IndexResolver;
import org.springframework.session.MapSession;
import org.springframework.session.PrincipalNameIndexResolver;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionOperations;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* A {@link org.springframework.session.SessionRepository} implementation that uses
* Spring's {@link JdbcOperations} to store sessions in a relational database. This
* implementation does not support publishing of session events.
* <p>
* An example of how to create a new instance can be seen below:
*
* <pre class="code">
* JdbcTemplate jdbcTemplate = new JdbcTemplate();
*
* // ... configure jdbcTemplate ...
*
* PlatformTransactionManager transactionManager = new DataSourceTransactionManager();
*
* // ... configure transactionManager ...
*
* JdbcOperationsSessionRepository sessionRepository =
* new JdbcOperationsSessionRepository(jdbcTemplate, transactionManager);
* </pre>
*
* For additional information on how to create and configure {@link JdbcTemplate} and
* {@link PlatformTransactionManager}, refer to the <a href=
* "https://docs.spring.io/spring/docs/current/spring-framework-reference/html/spring-data-tier.html">
* Spring Framework Reference Documentation</a>.
* <p>
* By default, this implementation uses <code>SPRING_SESSION</code> and
* <code>SPRING_SESSION_ATTRIBUTES</code> tables to store sessions. Note that the table
* name can be customized using the {@link #setTableName(String)} method. In that case the
* table used to store attributes will be named using the provided table name, suffixed
* with <code>_ATTRIBUTES</code>.
*
* Depending on your database, the table definition can be described as below:
*
* <pre class="code">
* CREATE TABLE SPRING_SESSION (
* PRIMARY_ID CHAR(36) NOT NULL,
* SESSION_ID CHAR(36) NOT NULL,
* CREATION_TIME BIGINT NOT NULL,
* LAST_ACCESS_TIME BIGINT NOT NULL,
* MAX_INACTIVE_INTERVAL INT NOT NULL,
* EXPIRY_TIME BIGINT NOT NULL,
* PRINCIPAL_NAME VARCHAR(100),
* CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (PRIMARY_ID)
* );
*
* CREATE UNIQUE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (SESSION_ID);
* CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (EXPIRY_TIME);
* CREATE INDEX SPRING_SESSION_IX3 ON SPRING_SESSION (PRINCIPAL_NAME);
*
* CREATE TABLE SPRING_SESSION_ATTRIBUTES (
* SESSION_PRIMARY_ID CHAR(36) NOT NULL,
* ATTRIBUTE_NAME VARCHAR(200) NOT NULL,
* ATTRIBUTE_BYTES BYTEA NOT NULL,
* CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_PRIMARY_ID, ATTRIBUTE_NAME),
* CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_PRIMARY_ID) REFERENCES SPRING_SESSION(PRIMARY_ID) ON DELETE CASCADE
* );
*
* CREATE INDEX SPRING_SESSION_ATTRIBUTES_IX1 ON SPRING_SESSION_ATTRIBUTES (SESSION_PRIMARY_ID);
* </pre>
*
* Due to the differences between the various database vendors, especially when it comes
* to storing binary data, make sure to use SQL script specific to your database. Scripts
* for most major database vendors are packaged as
* <code>org/springframework/session/jdbc/schema-*.sql</code>, where <code>*</code> is the
* target database type.
* This {@link SessionRepository} implementation is kept in order to support migration to
* {@link JdbcIndexedSessionRepository} in a backwards compatible manner.
*
* @author Vedran Pavic
* @author Craig Andrews
* @since 1.2.0
* @deprecated since 2.2.0 in favor of {@link JdbcIndexedSessionRepository}
*/
public class JdbcOperationsSessionRepository
implements FindByIndexNameSessionRepository<JdbcOperationsSessionRepository.JdbcSession> {
/**
* The default name of database table used by Spring Session to store sessions.
*/
public static final String DEFAULT_TABLE_NAME = "SPRING_SESSION";
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
// @formatter:off
private static final String CREATE_SESSION_QUERY = "INSERT INTO %TABLE_NAME%(PRIMARY_ID, SESSION_ID, CREATION_TIME, LAST_ACCESS_TIME, MAX_INACTIVE_INTERVAL, EXPIRY_TIME, PRINCIPAL_NAME) "
+ "VALUES (?, ?, ?, ?, ?, ?, ?)";
// @formatter:on
// @formatter:off
private static final String CREATE_SESSION_ATTRIBUTE_QUERY = "INSERT INTO %TABLE_NAME%_ATTRIBUTES(SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES) "
+ "SELECT PRIMARY_ID, ?, ? "
+ "FROM %TABLE_NAME% "
+ "WHERE SESSION_ID = ?";
// @formatter:on
// @formatter:off
private static final String GET_SESSION_QUERY = "SELECT S.PRIMARY_ID, S.SESSION_ID, S.CREATION_TIME, S.LAST_ACCESS_TIME, S.MAX_INACTIVE_INTERVAL, SA.ATTRIBUTE_NAME, SA.ATTRIBUTE_BYTES "
+ "FROM %TABLE_NAME% S "
+ "LEFT OUTER JOIN %TABLE_NAME%_ATTRIBUTES SA ON S.PRIMARY_ID = SA.SESSION_PRIMARY_ID "
+ "WHERE S.SESSION_ID = ?";
// @formatter:on
// @formatter:off
private static final String UPDATE_SESSION_QUERY = "UPDATE %TABLE_NAME% SET SESSION_ID = ?, LAST_ACCESS_TIME = ?, MAX_INACTIVE_INTERVAL = ?, EXPIRY_TIME = ?, PRINCIPAL_NAME = ? "
+ "WHERE PRIMARY_ID = ?";
// @formatter:on
// @formatter:off
private static final String UPDATE_SESSION_ATTRIBUTE_QUERY = "UPDATE %TABLE_NAME%_ATTRIBUTES SET ATTRIBUTE_BYTES = ? "
+ "WHERE SESSION_PRIMARY_ID = ? "
+ "AND ATTRIBUTE_NAME = ?";
// @formatter:on
// @formatter:off
private static final String DELETE_SESSION_ATTRIBUTE_QUERY = "DELETE FROM %TABLE_NAME%_ATTRIBUTES "
+ "WHERE SESSION_PRIMARY_ID = ? "
+ "AND ATTRIBUTE_NAME = ?";
// @formatter:on
// @formatter:off
private static final String DELETE_SESSION_QUERY = "DELETE FROM %TABLE_NAME% "
+ "WHERE SESSION_ID = ?";
// @formatter:on
// @formatter:off
private static final String LIST_SESSIONS_BY_PRINCIPAL_NAME_QUERY = "SELECT S.PRIMARY_ID, S.SESSION_ID, S.CREATION_TIME, S.LAST_ACCESS_TIME, S.MAX_INACTIVE_INTERVAL, SA.ATTRIBUTE_NAME, SA.ATTRIBUTE_BYTES "
+ "FROM %TABLE_NAME% S "
+ "LEFT OUTER JOIN %TABLE_NAME%_ATTRIBUTES SA ON S.PRIMARY_ID = SA.SESSION_PRIMARY_ID "
+ "WHERE S.PRINCIPAL_NAME = ?";
// @formatter:on
// @formatter:off
private static final String DELETE_SESSIONS_BY_EXPIRY_TIME_QUERY = "DELETE FROM %TABLE_NAME% "
+ "WHERE EXPIRY_TIME < ?";
// @formatter:on
private static final Log logger = LogFactory.getLog(JdbcOperationsSessionRepository.class);
private final JdbcOperations jdbcOperations;
private final ResultSetExtractor<List<JdbcSession>> extractor = new SessionResultSetExtractor();
private final IndexResolver<JdbcSession> indexResolver;
private TransactionOperations transactionOperations = TransactionOperations.withoutTransaction();
/**
* The name of database table used by Spring Session to store sessions.
*/
private String tableName = DEFAULT_TABLE_NAME;
private String createSessionQuery;
private String createSessionAttributeQuery;
private String getSessionQuery;
private String updateSessionQuery;
private String updateSessionAttributeQuery;
private String deleteSessionAttributeQuery;
private String deleteSessionQuery;
private String listSessionsByPrincipalNameQuery;
private String deleteSessionsByExpiryTimeQuery;
/**
* If non-null, this value is used to override the default value for
* {@link JdbcSession#setMaxInactiveInterval(Duration)}.
*/
private Integer defaultMaxInactiveInterval;
private ConversionService conversionService;
private LobHandler lobHandler = new DefaultLobHandler();
private FlushMode flushMode = FlushMode.ON_SAVE;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
@Deprecated
public class JdbcOperationsSessionRepository extends JdbcIndexedSessionRepository {
/**
* Create a new {@link JdbcOperationsSessionRepository} instance which uses the
* provided {@link JdbcOperations} to manage sessions.
* provided {@link JdbcOperations} and {@link TransactionOperations} to manage
* sessions.
* @param jdbcOperations the {@link JdbcOperations} to use
* @param transactionOperations the {@link TransactionOperations} to use
* @see JdbcIndexedSessionRepository#JdbcIndexedSessionRepository(JdbcOperations,
* TransactionOperations)
*/
public JdbcOperationsSessionRepository(JdbcOperations jdbcOperations, TransactionOperations transactionOperations) {
super(jdbcOperations, transactionOperations);
}
/**
* Create a new {@link JdbcIndexedSessionRepository} instance which uses the provided
* {@link JdbcOperations} to manage sessions.
* <p>
* The created instance will execute all data access operations in a transaction with
* propagation level of {@link TransactionDefinition#PROPAGATION_REQUIRES_NEW}.
* @param jdbcOperations the {@link JdbcOperations} to use
* @param transactionManager the {@link PlatformTransactionManager} to use
* @deprecated since 2.2.0 in favor of
* {@link JdbcIndexedSessionRepository#JdbcIndexedSessionRepository(JdbcOperations, TransactionOperations)}
*/
@Deprecated
public JdbcOperationsSessionRepository(JdbcOperations jdbcOperations,
PlatformTransactionManager transactionManager) {
this(jdbcOperations);
Assert.notNull(transactionManager, "TransactionManager must not be null");
this.transactionOperations = createTransactionTemplate(transactionManager);
super(jdbcOperations, createTransactionTemplate(transactionManager));
}
/**
* Create a new {@link JdbcOperationsSessionRepository} instance which uses the
* provided {@link JdbcOperations} to manage sessions.
* Create a new {@link JdbcIndexedSessionRepository} instance which uses the provided
* {@link JdbcOperations} to manage sessions.
* <p>
* The created instance will not execute data access operations in a transaction.
* @param jdbcOperations the {@link JdbcOperations} to use
* @deprecated since 2.2.0 in favor of
* {@link JdbcIndexedSessionRepository#JdbcIndexedSessionRepository(JdbcOperations, TransactionOperations)}
*/
@Deprecated
public JdbcOperationsSessionRepository(JdbcOperations jdbcOperations) {
Assert.notNull(jdbcOperations, "JdbcOperations must not be null");
this.jdbcOperations = jdbcOperations;
this.indexResolver = new DelegatingIndexResolver<>(new PrincipalNameIndexResolver<>());
this.conversionService = createDefaultConversionService();
prepareQueries();
}
/**
* Set the name of database table used to store sessions.
* @param tableName the database table name
*/
public void setTableName(String tableName) {
Assert.hasText(tableName, "Table name must not be empty");
this.tableName = tableName.trim();
prepareQueries();
}
/**
* Set the custom SQL query used to create the session.
* @param createSessionQuery the SQL query string
*/
public void setCreateSessionQuery(String createSessionQuery) {
Assert.hasText(createSessionQuery, "Query must not be empty");
this.createSessionQuery = createSessionQuery;
}
/**
* Set the custom SQL query used to create the session attribute.
* @param createSessionAttributeQuery the SQL query string
*/
public void setCreateSessionAttributeQuery(String createSessionAttributeQuery) {
Assert.hasText(createSessionAttributeQuery, "Query must not be empty");
this.createSessionAttributeQuery = createSessionAttributeQuery;
}
/**
* Set the custom SQL query used to retrieve the session.
* @param getSessionQuery the SQL query string
*/
public void setGetSessionQuery(String getSessionQuery) {
Assert.hasText(getSessionQuery, "Query must not be empty");
this.getSessionQuery = getSessionQuery;
}
/**
* Set the custom SQL query used to update the session.
* @param updateSessionQuery the SQL query string
*/
public void setUpdateSessionQuery(String updateSessionQuery) {
Assert.hasText(updateSessionQuery, "Query must not be empty");
this.updateSessionQuery = updateSessionQuery;
}
/**
* Set the custom SQL query used to update the session attribute.
* @param updateSessionAttributeQuery the SQL query string
*/
public void setUpdateSessionAttributeQuery(String updateSessionAttributeQuery) {
Assert.hasText(updateSessionAttributeQuery, "Query must not be empty");
this.updateSessionAttributeQuery = updateSessionAttributeQuery;
}
/**
* Set the custom SQL query used to delete the session attribute.
* @param deleteSessionAttributeQuery the SQL query string
*/
public void setDeleteSessionAttributeQuery(String deleteSessionAttributeQuery) {
Assert.hasText(deleteSessionAttributeQuery, "Query must not be empty");
this.deleteSessionAttributeQuery = deleteSessionAttributeQuery;
}
/**
* Set the custom SQL query used to delete the session.
* @param deleteSessionQuery the SQL query string
*/
public void setDeleteSessionQuery(String deleteSessionQuery) {
Assert.hasText(deleteSessionQuery, "Query must not be empty");
this.deleteSessionQuery = deleteSessionQuery;
}
/**
* Set the custom SQL query used to retrieve the sessions by principal name.
* @param listSessionsByPrincipalNameQuery the SQL query string
*/
public void setListSessionsByPrincipalNameQuery(String listSessionsByPrincipalNameQuery) {
Assert.hasText(listSessionsByPrincipalNameQuery, "Query must not be empty");
this.listSessionsByPrincipalNameQuery = listSessionsByPrincipalNameQuery;
}
/**
* Set the custom SQL query used to delete the sessions by last access time.
* @param deleteSessionsByExpiryTimeQuery the SQL query string
*/
public void setDeleteSessionsByExpiryTimeQuery(String deleteSessionsByExpiryTimeQuery) {
Assert.hasText(deleteSessionsByExpiryTimeQuery, "Query must not be empty");
this.deleteSessionsByExpiryTimeQuery = deleteSessionsByExpiryTimeQuery;
}
/**
* 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 void setLobHandler(LobHandler lobHandler) {
Assert.notNull(lobHandler, "LobHandler must not be null");
this.lobHandler = lobHandler;
}
/**
* Sets the {@link ConversionService} to use.
* @param conversionService the converter to set
*/
public void setConversionService(ConversionService conversionService) {
Assert.notNull(conversionService, "conversionService must not be null");
this.conversionService = conversionService;
}
/**
* Set the flush mode. Default is {@link FlushMode#ON_SAVE}.
* @param flushMode the flush mode
*/
public void setFlushMode(FlushMode flushMode) {
Assert.notNull(flushMode, "flushMode must not be null");
this.flushMode = flushMode;
}
/**
* Set the save mode.
* @param saveMode the save mode
*/
public void setSaveMode(SaveMode saveMode) {
Assert.notNull(saveMode, "saveMode must not be null");
this.saveMode = saveMode;
}
@Override
public JdbcSession createSession() {
MapSession delegate = new MapSession();
if (this.defaultMaxInactiveInterval != null) {
delegate.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
}
JdbcSession session = new JdbcSession(delegate, UUID.randomUUID().toString(), true);
session.flushIfRequired();
return session;
}
@Override
public void save(final JdbcSession session) {
session.save();
}
@Override
public JdbcSession findById(final String id) {
final JdbcSession session = this.transactionOperations.execute((status) -> {
List<JdbcSession> sessions = JdbcOperationsSessionRepository.this.jdbcOperations.query(
JdbcOperationsSessionRepository.this.getSessionQuery, (ps) -> ps.setString(1, id),
JdbcOperationsSessionRepository.this.extractor);
if (sessions.isEmpty()) {
return null;
}
return sessions.get(0);
});
if (session != null) {
if (session.isExpired()) {
deleteById(id);
}
else {
return session;
}
}
return null;
}
@Override
public void deleteById(final String id) {
this.transactionOperations.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
JdbcOperationsSessionRepository.this.jdbcOperations
.update(JdbcOperationsSessionRepository.this.deleteSessionQuery, id);
}
});
}
@Override
public Map<String, JdbcSession> findByIndexNameAndIndexValue(String indexName, final String indexValue) {
if (!PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) {
return Collections.emptyMap();
}
List<JdbcSession> sessions = this.transactionOperations
.execute((status) -> JdbcOperationsSessionRepository.this.jdbcOperations.query(
JdbcOperationsSessionRepository.this.listSessionsByPrincipalNameQuery,
(ps) -> ps.setString(1, indexValue), JdbcOperationsSessionRepository.this.extractor));
Map<String, JdbcSession> sessionMap = new HashMap<>(sessions.size());
for (JdbcSession session : sessions) {
sessionMap.put(session.getId(), session);
}
return sessionMap;
}
private void insertSessionAttributes(JdbcSession session, List<String> attributeNames) {
Assert.notEmpty(attributeNames, "attributeNames must not be null or empty");
if (attributeNames.size() > 1) {
this.jdbcOperations.batchUpdate(this.createSessionAttributeQuery, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
String attributeName = attributeNames.get(i);
ps.setString(1, attributeName);
getLobHandler().getLobCreator().setBlobAsBytes(ps, 2,
serialize(session.getAttribute(attributeName)));
ps.setString(3, session.getId());
}
@Override
public int getBatchSize() {
return attributeNames.size();
}
});
}
else {
this.jdbcOperations.update(this.createSessionAttributeQuery, (ps) -> {
String attributeName = attributeNames.get(0);
ps.setString(1, attributeName);
getLobHandler().getLobCreator().setBlobAsBytes(ps, 2, serialize(session.getAttribute(attributeName)));
ps.setString(3, session.getId());
});
}
}
private void updateSessionAttributes(JdbcSession session, List<String> attributeNames) {
Assert.notEmpty(attributeNames, "attributeNames must not be null or empty");
if (attributeNames.size() > 1) {
this.jdbcOperations.batchUpdate(this.updateSessionAttributeQuery, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
String attributeName = attributeNames.get(i);
getLobHandler().getLobCreator().setBlobAsBytes(ps, 1,
serialize(session.getAttribute(attributeName)));
ps.setString(2, session.primaryKey);
ps.setString(3, attributeName);
}
@Override
public int getBatchSize() {
return attributeNames.size();
}
});
}
else {
this.jdbcOperations.update(this.updateSessionAttributeQuery, (ps) -> {
String attributeName = attributeNames.get(0);
getLobHandler().getLobCreator().setBlobAsBytes(ps, 1, serialize(session.getAttribute(attributeName)));
ps.setString(2, session.primaryKey);
ps.setString(3, attributeName);
});
}
}
private void deleteSessionAttributes(JdbcSession session, List<String> attributeNames) {
Assert.notEmpty(attributeNames, "attributeNames must not be null or empty");
if (attributeNames.size() > 1) {
this.jdbcOperations.batchUpdate(this.deleteSessionAttributeQuery, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
String attributeName = attributeNames.get(i);
ps.setString(1, session.primaryKey);
ps.setString(2, attributeName);
}
@Override
public int getBatchSize() {
return attributeNames.size();
}
});
}
else {
this.jdbcOperations.update(this.deleteSessionAttributeQuery, (ps) -> {
String attributeName = attributeNames.get(0);
ps.setString(1, session.primaryKey);
ps.setString(2, attributeName);
});
}
}
public void cleanUpExpiredSessions() {
Integer deletedCount = this.transactionOperations
.execute((status) -> JdbcOperationsSessionRepository.this.jdbcOperations.update(
JdbcOperationsSessionRepository.this.deleteSessionsByExpiryTimeQuery,
System.currentTimeMillis()));
if (logger.isDebugEnabled()) {
logger.debug("Cleaned up " + deletedCount + " expired sessions");
}
super(jdbcOperations, TransactionOperations.withoutTransaction());
}
private static TransactionTemplate createTransactionTemplate(PlatformTransactionManager transactionManager) {
Assert.notNull(transactionManager, "transactionManager must not be null");
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
transactionTemplate.afterPropertiesSet();
return transactionTemplate;
}
private static GenericConversionService createDefaultConversionService() {
GenericConversionService converter = new GenericConversionService();
converter.addConverter(Object.class, byte[].class, new SerializingConverter());
converter.addConverter(byte[].class, Object.class, new DeserializingConverter());
return converter;
}
private String getQuery(String base) {
return StringUtils.replace(base, "%TABLE_NAME%", this.tableName);
}
private void prepareQueries() {
this.createSessionQuery = getQuery(CREATE_SESSION_QUERY);
this.createSessionAttributeQuery = getQuery(CREATE_SESSION_ATTRIBUTE_QUERY);
this.getSessionQuery = getQuery(GET_SESSION_QUERY);
this.updateSessionQuery = getQuery(UPDATE_SESSION_QUERY);
this.updateSessionAttributeQuery = getQuery(UPDATE_SESSION_ATTRIBUTE_QUERY);
this.deleteSessionAttributeQuery = getQuery(DELETE_SESSION_ATTRIBUTE_QUERY);
this.deleteSessionQuery = getQuery(DELETE_SESSION_QUERY);
this.listSessionsByPrincipalNameQuery = getQuery(LIST_SESSIONS_BY_PRINCIPAL_NAME_QUERY);
this.deleteSessionsByExpiryTimeQuery = getQuery(DELETE_SESSIONS_BY_EXPIRY_TIME_QUERY);
}
private LobHandler getLobHandler() {
return this.lobHandler;
}
private byte[] serialize(Object object) {
return (byte[]) this.conversionService.convert(object, TypeDescriptor.valueOf(Object.class),
TypeDescriptor.valueOf(byte[].class));
}
private Object deserialize(byte[] bytes) {
return this.conversionService.convert(bytes, TypeDescriptor.valueOf(byte[].class),
TypeDescriptor.valueOf(Object.class));
}
private enum DeltaValue {
ADDED, UPDATED, REMOVED
}
private static <T> Supplier<T> value(T value) {
return (value != null) ? () -> value : null;
}
private static <T> Supplier<T> lazily(Supplier<T> supplier) {
Supplier<T> lazySupplier = new Supplier<T>() {
private T value;
@Override
public T get() {
if (this.value == null) {
this.value = supplier.get();
}
return this.value;
}
};
return (supplier != null) ? lazySupplier : null;
}
/**
* The {@link Session} to use for {@link JdbcOperationsSessionRepository}.
*
* @author Vedran Pavic
*/
final class JdbcSession implements Session {
private final Session delegate;
private final String primaryKey;
private boolean isNew;
private boolean changed;
private Map<String, DeltaValue> delta = new HashMap<>();
JdbcSession(MapSession delegate, String primaryKey, boolean isNew) {
this.delegate = delegate;
this.primaryKey = primaryKey;
this.isNew = isNew;
if (this.isNew || (JdbcOperationsSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
getAttributeNames().forEach((attributeName) -> this.delta.put(attributeName, DeltaValue.UPDATED));
}
}
boolean isNew() {
return this.isNew;
}
boolean isChanged() {
return this.changed;
}
Map<String, DeltaValue> getDelta() {
return this.delta;
}
void clearChangeFlags() {
this.isNew = false;
this.changed = false;
this.delta.clear();
}
Instant getExpiryTime() {
return getLastAccessedTime().plus(getMaxInactiveInterval());
}
@Override
public String getId() {
return this.delegate.getId();
}
@Override
public String changeSessionId() {
this.changed = true;
return this.delegate.changeSessionId();
}
@Override
public <T> T getAttribute(String attributeName) {
Supplier<T> supplier = this.delegate.getAttribute(attributeName);
if (supplier == null) {
return null;
}
T attributeValue = supplier.get();
if (attributeValue != null
&& JdbcOperationsSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
this.delta.put(attributeName, DeltaValue.UPDATED);
}
return attributeValue;
}
@Override
public Set<String> getAttributeNames() {
return this.delegate.getAttributeNames();
}
@Override
public void setAttribute(String attributeName, Object attributeValue) {
boolean attributeExists = (this.delegate.getAttribute(attributeName) != null);
boolean attributeRemoved = (attributeValue == null);
if (!attributeExists && attributeRemoved) {
return;
}
if (attributeExists) {
if (attributeRemoved) {
this.delta.merge(attributeName, DeltaValue.REMOVED,
(oldDeltaValue, deltaValue) -> (oldDeltaValue == DeltaValue.ADDED) ? null : deltaValue);
}
else {
this.delta.merge(attributeName, DeltaValue.UPDATED, (oldDeltaValue,
deltaValue) -> (oldDeltaValue == DeltaValue.ADDED) ? oldDeltaValue : deltaValue);
}
}
else {
this.delta.merge(attributeName, DeltaValue.ADDED, (oldDeltaValue,
deltaValue) -> (oldDeltaValue == DeltaValue.ADDED) ? oldDeltaValue : DeltaValue.UPDATED);
}
this.delegate.setAttribute(attributeName, value(attributeValue));
if (PRINCIPAL_NAME_INDEX_NAME.equals(attributeName) || SPRING_SECURITY_CONTEXT.equals(attributeName)) {
this.changed = true;
}
flushIfRequired();
}
@Override
public void removeAttribute(String attributeName) {
setAttribute(attributeName, null);
}
@Override
public Instant getCreationTime() {
return this.delegate.getCreationTime();
}
@Override
public void setLastAccessedTime(Instant lastAccessedTime) {
this.delegate.setLastAccessedTime(lastAccessedTime);
this.changed = true;
flushIfRequired();
}
@Override
public Instant getLastAccessedTime() {
return this.delegate.getLastAccessedTime();
}
@Override
public void setMaxInactiveInterval(Duration interval) {
this.delegate.setMaxInactiveInterval(interval);
this.changed = true;
flushIfRequired();
}
@Override
public Duration getMaxInactiveInterval() {
return this.delegate.getMaxInactiveInterval();
}
@Override
public boolean isExpired() {
return this.delegate.isExpired();
}
private void flushIfRequired() {
if (JdbcOperationsSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {
save();
}
}
private void save() {
if (this.isNew) {
JdbcOperationsSessionRepository.this.transactionOperations
.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
Map<String, String> indexes = JdbcOperationsSessionRepository.this.indexResolver
.resolveIndexesFor(JdbcSession.this);
JdbcOperationsSessionRepository.this.jdbcOperations
.update(JdbcOperationsSessionRepository.this.createSessionQuery, (ps) -> {
ps.setString(1, JdbcSession.this.primaryKey);
ps.setString(2, getId());
ps.setLong(3, getCreationTime().toEpochMilli());
ps.setLong(4, getLastAccessedTime().toEpochMilli());
ps.setInt(5, (int) getMaxInactiveInterval().getSeconds());
ps.setLong(6, getExpiryTime().toEpochMilli());
ps.setString(7, indexes.get(PRINCIPAL_NAME_INDEX_NAME));
});
Set<String> attributeNames = getAttributeNames();
if (!attributeNames.isEmpty()) {
insertSessionAttributes(JdbcSession.this, new ArrayList<>(attributeNames));
}
}
});
}
else {
JdbcOperationsSessionRepository.this.transactionOperations
.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
if (JdbcSession.this.changed) {
Map<String, String> indexes = JdbcOperationsSessionRepository.this.indexResolver
.resolveIndexesFor(JdbcSession.this);
JdbcOperationsSessionRepository.this.jdbcOperations
.update(JdbcOperationsSessionRepository.this.updateSessionQuery, (ps) -> {
ps.setString(1, getId());
ps.setLong(2, getLastAccessedTime().toEpochMilli());
ps.setInt(3, (int) getMaxInactiveInterval().getSeconds());
ps.setLong(4, getExpiryTime().toEpochMilli());
ps.setString(5, indexes.get(PRINCIPAL_NAME_INDEX_NAME));
ps.setString(6, JdbcSession.this.primaryKey);
});
}
List<String> addedAttributeNames = JdbcSession.this.delta.entrySet().stream()
.filter((entry) -> entry.getValue() == DeltaValue.ADDED).map(Map.Entry::getKey)
.collect(Collectors.toList());
if (!addedAttributeNames.isEmpty()) {
insertSessionAttributes(JdbcSession.this, addedAttributeNames);
}
List<String> updatedAttributeNames = JdbcSession.this.delta.entrySet().stream()
.filter((entry) -> entry.getValue() == DeltaValue.UPDATED)
.map(Map.Entry::getKey).collect(Collectors.toList());
if (!updatedAttributeNames.isEmpty()) {
updateSessionAttributes(JdbcSession.this, updatedAttributeNames);
}
List<String> removedAttributeNames = JdbcSession.this.delta.entrySet().stream()
.filter((entry) -> entry.getValue() == DeltaValue.REMOVED)
.map(Map.Entry::getKey).collect(Collectors.toList());
if (!removedAttributeNames.isEmpty()) {
deleteSessionAttributes(JdbcSession.this, removedAttributeNames);
}
}
});
}
clearChangeFlags();
}
}
private class SessionResultSetExtractor implements ResultSetExtractor<List<JdbcSession>> {
@Override
public List<JdbcSession> extractData(ResultSet rs) throws SQLException, DataAccessException {
List<JdbcSession> sessions = new ArrayList<>();
while (rs.next()) {
String id = rs.getString("SESSION_ID");
JdbcSession session;
if (sessions.size() > 0 && getLast(sessions).getId().equals(id)) {
session = getLast(sessions);
}
else {
MapSession delegate = new MapSession(id);
String primaryKey = rs.getString("PRIMARY_ID");
delegate.setCreationTime(Instant.ofEpochMilli(rs.getLong("CREATION_TIME")));
delegate.setLastAccessedTime(Instant.ofEpochMilli(rs.getLong("LAST_ACCESS_TIME")));
delegate.setMaxInactiveInterval(Duration.ofSeconds(rs.getInt("MAX_INACTIVE_INTERVAL")));
session = new JdbcSession(delegate, primaryKey, false);
}
String attributeName = rs.getString("ATTRIBUTE_NAME");
if (attributeName != null) {
byte[] bytes = getLobHandler().getBlobAsBytes(rs, "ATTRIBUTE_BYTES");
session.delegate.setAttribute(attributeName, lazily(() -> deserialize(bytes)));
}
sessions.add(session);
}
return sessions;
}
private JdbcSession getLast(List<JdbcSession> sessions) {
return sessions.get(sessions.size() - 1);
}
}
}

View File

@@ -25,11 +25,11 @@ import java.lang.annotation.Target;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.session.jdbc.JdbcOperationsSessionRepository;
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
/**
* Qualifier annotation for a {@link DataSource} to be injected in
* {@link JdbcOperationsSessionRepository}.
* {@link JdbcIndexedSessionRepository}.
*
* @author Vedran Pavic
* @since 2.0.0

View File

@@ -32,7 +32,7 @@ import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
import org.springframework.session.jdbc.JdbcOperationsSessionRepository;
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
/**
@@ -77,7 +77,7 @@ import org.springframework.session.web.http.SessionRepositoryFilter;
@Target(ElementType.TYPE)
@Documented
@Import(JdbcHttpSessionConfiguration.class)
@Configuration
@Configuration(proxyBeanMethods = false)
public @interface EnableJdbcHttpSession {
/**
@@ -91,7 +91,7 @@ public @interface EnableJdbcHttpSession {
* The name of database table used by Spring Session to store sessions.
* @return the database table name
*/
String tableName() default JdbcOperationsSessionRepository.DEFAULT_TABLE_NAME;
String tableName() default JdbcIndexedSessionRepository.DEFAULT_TABLE_NAME;
/**
* The cron expression for expired session cleanup job. By default runs every minute.

View File

@@ -16,7 +16,9 @@
package org.springframework.session.jdbc.config.annotation.web.http;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.sql.DataSource;
@@ -43,13 +45,19 @@ import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.session.FlushMode;
import org.springframework.session.IndexResolver;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.config.SessionRepositoryCustomizer;
import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration;
import org.springframework.session.jdbc.JdbcOperationsSessionRepository;
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
import org.springframework.session.jdbc.config.annotation.SpringSessionDataSource;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionOperations;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.StringUtils;
import org.springframework.util.StringValueResolver;
@@ -67,15 +75,14 @@ import org.springframework.util.StringValueResolver;
* @see EnableJdbcHttpSession
*/
@Configuration(proxyBeanMethods = false)
@EnableScheduling
public class JdbcHttpSessionConfiguration extends SpringHttpSessionConfiguration
implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware, SchedulingConfigurer {
implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware {
static final String DEFAULT_CLEANUP_CRON = "0 * * * * *";
private Integer maxInactiveIntervalInSeconds = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
private String tableName = JdbcOperationsSessionRepository.DEFAULT_TABLE_NAME;
private String tableName = JdbcIndexedSessionRepository.DEFAULT_TABLE_NAME;
private String cleanupCron = DEFAULT_CLEANUP_CRON;
@@ -87,27 +94,39 @@ public class JdbcHttpSessionConfiguration extends SpringHttpSessionConfiguration
private PlatformTransactionManager transactionManager;
private TransactionOperations transactionOperations;
private IndexResolver<Session> indexResolver;
private LobHandler lobHandler;
private ConversionService springSessionConversionService;
private ConversionService conversionService;
private List<SessionRepositoryCustomizer<JdbcIndexedSessionRepository>> sessionRepositoryCustomizers;
private ClassLoader classLoader;
private StringValueResolver embeddedValueResolver;
@Bean
public JdbcOperationsSessionRepository sessionRepository() {
public JdbcIndexedSessionRepository sessionRepository() {
JdbcTemplate jdbcTemplate = createJdbcTemplate(this.dataSource);
JdbcOperationsSessionRepository sessionRepository = new JdbcOperationsSessionRepository(jdbcTemplate,
this.transactionManager);
if (this.transactionOperations == null) {
this.transactionOperations = createTransactionTemplate(this.transactionManager);
}
JdbcIndexedSessionRepository sessionRepository = new JdbcIndexedSessionRepository(jdbcTemplate,
this.transactionOperations);
if (StringUtils.hasText(this.tableName)) {
sessionRepository.setTableName(this.tableName);
}
sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
sessionRepository.setFlushMode(this.flushMode);
sessionRepository.setSaveMode(this.saveMode);
if (this.indexResolver != null) {
sessionRepository.setIndexResolver(this.indexResolver);
}
if (this.lobHandler != null) {
sessionRepository.setLobHandler(this.lobHandler);
}
@@ -123,8 +142,10 @@ public class JdbcHttpSessionConfiguration extends SpringHttpSessionConfiguration
sessionRepository.setConversionService(this.conversionService);
}
else {
sessionRepository.setConversionService(createConversionServiceWithBeanClassLoader());
sessionRepository.setConversionService(createConversionServiceWithBeanClassLoader(this.classLoader));
}
this.sessionRepositoryCustomizers
.forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository));
return sessionRepository;
}
@@ -173,6 +194,17 @@ public class JdbcHttpSessionConfiguration extends SpringHttpSessionConfiguration
this.transactionManager = transactionManager;
}
@Autowired(required = false)
@Qualifier("springSessionTransactionOperations")
public void setTransactionOperations(TransactionOperations transactionOperations) {
this.transactionOperations = transactionOperations;
}
@Autowired(required = false)
public void setIndexResolver(IndexResolver<Session> indexResolver) {
this.indexResolver = indexResolver;
}
@Autowired(required = false)
@Qualifier("springSessionLobHandler")
public void setLobHandler(LobHandler lobHandler) {
@@ -191,6 +223,12 @@ public class JdbcHttpSessionConfiguration extends SpringHttpSessionConfiguration
this.conversionService = conversionService;
}
@Autowired(required = false)
public void setSessionRepositoryCustomizer(
ObjectProvider<SessionRepositoryCustomizer<JdbcIndexedSessionRepository>> sessionRepositoryCustomizers) {
this.sessionRepositoryCustomizers = sessionRepositoryCustomizers.orderedStream().collect(Collectors.toList());
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
@@ -219,22 +257,45 @@ public class JdbcHttpSessionConfiguration extends SpringHttpSessionConfiguration
this.saveMode = attributes.getEnum("saveMode");
}
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.addCronTask(() -> sessionRepository().cleanUpExpiredSessions(), this.cleanupCron);
}
private static JdbcTemplate createJdbcTemplate(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.afterPropertiesSet();
return jdbcTemplate;
}
private GenericConversionService createConversionServiceWithBeanClassLoader() {
private static TransactionTemplate createTransactionTemplate(PlatformTransactionManager transactionManager) {
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
transactionTemplate.afterPropertiesSet();
return transactionTemplate;
}
private static GenericConversionService createConversionServiceWithBeanClassLoader(ClassLoader classLoader) {
GenericConversionService conversionService = new GenericConversionService();
conversionService.addConverter(Object.class, byte[].class, new SerializingConverter());
conversionService.addConverter(byte[].class, Object.class, new DeserializingConverter(this.classLoader));
conversionService.addConverter(byte[].class, Object.class, new DeserializingConverter(classLoader));
return conversionService;
}
/**
* Configuration of scheduled job for cleaning up expired sessions.
*/
@EnableScheduling
@Configuration(proxyBeanMethods = false)
class SessionCleanupConfiguration implements SchedulingConfigurer {
private final JdbcIndexedSessionRepository sessionRepository;
SessionCleanupConfiguration(JdbcIndexedSessionRepository sessionRepository) {
this.sessionRepository = sessionRepository;
}
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.addCronTask(this.sessionRepository::cleanUpExpiredSessions,
JdbcHttpSessionConfiguration.this.cleanupCron);
}
}
}

View File

@@ -28,6 +28,8 @@ import java.util.function.Supplier;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcOperations;
@@ -41,61 +43,54 @@ import org.springframework.session.FlushMode;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.jdbc.JdbcOperationsSessionRepository.JdbcSession;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.session.jdbc.JdbcIndexedSessionRepository.JdbcSession;
import org.springframework.transaction.support.TransactionOperations;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.endsWith;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.ArgumentMatchers.startsWith;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
/**
* Tests for {@link JdbcOperationsSessionRepository}.
* Tests for {@link JdbcIndexedSessionRepository}.
*
* @author Vedran Pavic
* @author Craig Andrews
* @since 1.2.0
*/
class JdbcOperationsSessionRepositoryTests {
class JdbcIndexedSessionRepositoryTests {
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
private JdbcOperations jdbcOperations = mock(JdbcOperations.class);
@Mock
private JdbcOperations jdbcOperations;
private PlatformTransactionManager transactionManager = mock(PlatformTransactionManager.class);
private JdbcOperationsSessionRepository repository;
private JdbcIndexedSessionRepository repository;
@BeforeEach
void setUp() {
this.repository = new JdbcOperationsSessionRepository(this.jdbcOperations, this.transactionManager);
MockitoAnnotations.initMocks(this);
this.repository = new JdbcIndexedSessionRepository(this.jdbcOperations,
TransactionOperations.withoutTransaction());
}
@Test
void constructorNullJdbcOperations() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new JdbcOperationsSessionRepository(null, this.transactionManager))
.withMessage("JdbcOperations must not be null");
.isThrownBy(() -> new JdbcIndexedSessionRepository(null, TransactionOperations.withoutTransaction()))
.withMessage("jdbcOperations must not be null");
}
@Test
void constructorNullTransactionManager() {
void constructorNullTransactionOperations() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new JdbcOperationsSessionRepository(this.jdbcOperations, null))
.withMessage("TransactionManager must not be null");
.isThrownBy(() -> new JdbcIndexedSessionRepository(this.jdbcOperations, null))
.withMessage("transactionOperations must not be null");
}
@Test
@@ -244,11 +239,11 @@ class JdbcOperationsSessionRepositoryTests {
@Test
void createSessionDefaultMaxInactiveInterval() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
JdbcSession session = this.repository.createSession();
assertThat(session.isNew()).isTrue();
assertThat(session.getMaxInactiveInterval()).isEqualTo(new MapSession().getMaxInactiveInterval());
verifyZeroInteractions(this.jdbcOperations);
verifyNoMoreInteractions(this.jdbcOperations);
}
@Test
@@ -256,11 +251,11 @@ class JdbcOperationsSessionRepositoryTests {
int interval = 1;
this.repository.setDefaultMaxInactiveInterval(interval);
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
JdbcSession session = this.repository.createSession();
assertThat(session.isNew()).isTrue();
assertThat(session.getMaxInactiveInterval()).isEqualTo(Duration.ofSeconds(interval));
verifyZeroInteractions(this.jdbcOperations);
verifyNoMoreInteractions(this.jdbcOperations);
}
@Test
@@ -268,91 +263,82 @@ class JdbcOperationsSessionRepositoryTests {
this.repository.setFlushMode(FlushMode.IMMEDIATE);
JdbcSession session = this.repository.createSession();
assertThat(session.isNew()).isFalse();
assertPropagationRequiresNew();
verify(this.jdbcOperations).update(startsWith("INSERT"), isA(PreparedStatementSetter.class));
verifyNoMoreInteractions(this.jdbcOperations);
}
@Test
void saveNewWithoutAttributes() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
JdbcSession session = this.repository.createSession();
this.repository.save(session);
assertThat(session.isNew()).isFalse();
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).update(startsWith("INSERT"), isA(PreparedStatementSetter.class));
verifyZeroInteractions(this.jdbcOperations);
verifyNoMoreInteractions(this.jdbcOperations);
}
@Test
void saveNewWithSingleAttribute() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
JdbcSession session = this.repository.createSession();
session.setAttribute("testName", "testValue");
this.repository.save(session);
assertThat(session.isNew()).isFalse();
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).update(startsWith("INSERT INTO SPRING_SESSION("),
isA(PreparedStatementSetter.class));
verify(this.jdbcOperations, times(1)).update(startsWith("INSERT INTO SPRING_SESSION_ATTRIBUTES("),
isA(PreparedStatementSetter.class));
verifyZeroInteractions(this.jdbcOperations);
verifyNoMoreInteractions(this.jdbcOperations);
}
@Test
void saveNewWithMultipleAttributes() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
JdbcSession session = this.repository.createSession();
session.setAttribute("testName1", "testValue1");
session.setAttribute("testName2", "testValue2");
this.repository.save(session);
assertThat(session.isNew()).isFalse();
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).update(startsWith("INSERT INTO SPRING_SESSION("),
isA(PreparedStatementSetter.class));
verify(this.jdbcOperations, times(1)).batchUpdate(startsWith("INSERT INTO SPRING_SESSION_ATTRIBUTES("),
isA(BatchPreparedStatementSetter.class));
verifyZeroInteractions(this.jdbcOperations);
verifyNoMoreInteractions(this.jdbcOperations);
}
@Test
void saveUpdatedAddSingleAttribute() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
"primaryKey", false);
JdbcSession session = this.repository.new JdbcSession(new MapSession(), "primaryKey", false);
session.setAttribute("testName", "testValue");
this.repository.save(session);
assertThat(session.isNew()).isFalse();
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).update(startsWith("INSERT INTO SPRING_SESSION_ATTRIBUTES("),
isA(PreparedStatementSetter.class));
verifyZeroInteractions(this.jdbcOperations);
verifyNoMoreInteractions(this.jdbcOperations);
}
@Test
void saveUpdatedAddMultipleAttributes() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
"primaryKey", false);
JdbcSession session = this.repository.new JdbcSession(new MapSession(), "primaryKey", false);
session.setAttribute("testName1", "testValue1");
session.setAttribute("testName2", "testValue2");
this.repository.save(session);
assertThat(session.isNew()).isFalse();
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).batchUpdate(startsWith("INSERT INTO SPRING_SESSION_ATTRIBUTES("),
isA(BatchPreparedStatementSetter.class));
verifyZeroInteractions(this.jdbcOperations);
verifyNoMoreInteractions(this.jdbcOperations);
}
@Test
void saveUpdatedModifySingleAttribute() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
"primaryKey", false);
JdbcSession session = this.repository.new JdbcSession(new MapSession(), "primaryKey", false);
session.setAttribute("testName", "testValue");
session.clearChangeFlags();
session.setAttribute("testName", "testValue");
@@ -360,16 +346,14 @@ class JdbcOperationsSessionRepositoryTests {
this.repository.save(session);
assertThat(session.isNew()).isFalse();
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).update(startsWith("UPDATE SPRING_SESSION_ATTRIBUTES SET"),
isA(PreparedStatementSetter.class));
verifyZeroInteractions(this.jdbcOperations);
verifyNoMoreInteractions(this.jdbcOperations);
}
@Test
void saveUpdatedModifyMultipleAttributes() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
"primaryKey", false);
JdbcSession session = this.repository.new JdbcSession(new MapSession(), "primaryKey", false);
session.setAttribute("testName1", "testValue1");
session.setAttribute("testName2", "testValue2");
session.clearChangeFlags();
@@ -379,16 +363,14 @@ class JdbcOperationsSessionRepositoryTests {
this.repository.save(session);
assertThat(session.isNew()).isFalse();
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).batchUpdate(startsWith("UPDATE SPRING_SESSION_ATTRIBUTES SET"),
isA(BatchPreparedStatementSetter.class));
verifyZeroInteractions(this.jdbcOperations);
verifyNoMoreInteractions(this.jdbcOperations);
}
@Test
void saveUpdatedRemoveSingleAttribute() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
"primaryKey", false);
JdbcSession session = this.repository.new JdbcSession(new MapSession(), "primaryKey", false);
session.setAttribute("testName", "testValue");
session.clearChangeFlags();
session.removeAttribute("testName");
@@ -396,29 +378,25 @@ class JdbcOperationsSessionRepositoryTests {
this.repository.save(session);
assertThat(session.isNew()).isFalse();
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).update(startsWith("DELETE FROM SPRING_SESSION_ATTRIBUTES WHERE"),
isA(PreparedStatementSetter.class));
verifyZeroInteractions(this.jdbcOperations);
verifyNoMoreInteractions(this.jdbcOperations);
}
@Test
void saveUpdatedRemoveNonExistingAttribute() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
"primaryKey", false);
JdbcSession session = this.repository.new JdbcSession(new MapSession(), "primaryKey", false);
session.removeAttribute("testName");
this.repository.save(session);
assertThat(session.isNew()).isFalse();
assertPropagationRequiresNew();
verifyZeroInteractions(this.jdbcOperations);
verifyNoMoreInteractions(this.jdbcOperations);
}
@Test
void saveUpdatedRemoveMultipleAttributes() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
"primaryKey", false);
JdbcSession session = this.repository.new JdbcSession(new MapSession(), "primaryKey", false);
session.setAttribute("testName1", "testValue1");
session.setAttribute("testName2", "testValue2");
session.clearChangeFlags();
@@ -428,46 +406,40 @@ class JdbcOperationsSessionRepositoryTests {
this.repository.save(session);
assertThat(session.isNew()).isFalse();
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).batchUpdate(startsWith("DELETE FROM SPRING_SESSION_ATTRIBUTES WHERE"),
isA(BatchPreparedStatementSetter.class));
verifyZeroInteractions(this.jdbcOperations);
verifyNoMoreInteractions(this.jdbcOperations);
}
@Test // gh-1070
void saveUpdatedAddAndModifyAttribute() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
"primaryKey", false);
JdbcSession session = this.repository.new JdbcSession(new MapSession(), "primaryKey", false);
session.setAttribute("testName", "testValue1");
session.setAttribute("testName", "testValue2");
this.repository.save(session);
assertThat(session.isNew()).isFalse();
assertPropagationRequiresNew();
verify(this.jdbcOperations).update(startsWith("INSERT INTO SPRING_SESSION_ATTRIBUTES("),
isA(PreparedStatementSetter.class));
verifyZeroInteractions(this.jdbcOperations);
verifyNoMoreInteractions(this.jdbcOperations);
}
@Test // gh-1070
void saveUpdatedAddAndRemoveAttribute() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
"primaryKey", false);
JdbcSession session = this.repository.new JdbcSession(new MapSession(), "primaryKey", false);
session.setAttribute("testName", "testValue");
session.removeAttribute("testName");
this.repository.save(session);
assertThat(session.isNew()).isFalse();
assertPropagationRequiresNew();
verifyZeroInteractions(this.jdbcOperations);
verifyNoMoreInteractions(this.jdbcOperations);
}
@Test // gh-1070
void saveUpdatedModifyAndRemoveAttribute() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
"primaryKey", false);
JdbcSession session = this.repository.new JdbcSession(new MapSession(), "primaryKey", false);
session.setAttribute("testName", "testValue1");
session.clearChangeFlags();
session.setAttribute("testName", "testValue2");
@@ -476,16 +448,14 @@ class JdbcOperationsSessionRepositoryTests {
this.repository.save(session);
assertThat(session.isNew()).isFalse();
assertPropagationRequiresNew();
verify(this.jdbcOperations).update(startsWith("DELETE FROM SPRING_SESSION_ATTRIBUTES WHERE"),
isA(PreparedStatementSetter.class));
verifyZeroInteractions(this.jdbcOperations);
verifyNoMoreInteractions(this.jdbcOperations);
}
@Test // gh-1070
void saveUpdatedRemoveAndAddAttribute() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
"primaryKey", false);
JdbcSession session = this.repository.new JdbcSession(new MapSession(), "primaryKey", false);
session.setAttribute("testName", "testValue1");
session.clearChangeFlags();
session.removeAttribute("testName");
@@ -494,36 +464,32 @@ class JdbcOperationsSessionRepositoryTests {
this.repository.save(session);
assertThat(session.isNew()).isFalse();
assertPropagationRequiresNew();
verify(this.jdbcOperations).update(startsWith("UPDATE SPRING_SESSION_ATTRIBUTES SET"),
isA(PreparedStatementSetter.class));
verifyZeroInteractions(this.jdbcOperations);
verifyNoMoreInteractions(this.jdbcOperations);
}
@Test
void saveUpdatedLastAccessedTime() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
"primaryKey", false);
JdbcSession session = this.repository.new JdbcSession(new MapSession(), "primaryKey", false);
session.setLastAccessedTime(Instant.now());
this.repository.save(session);
assertThat(session.isNew()).isFalse();
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).update(startsWith("UPDATE SPRING_SESSION SET"),
isA(PreparedStatementSetter.class));
verifyZeroInteractions(this.jdbcOperations);
verifyNoMoreInteractions(this.jdbcOperations);
}
@Test
void saveUnchanged() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
"primaryKey", false);
JdbcSession session = this.repository.new JdbcSession(new MapSession(), "primaryKey", false);
this.repository.save(session);
assertThat(session.isNew()).isFalse();
verifyZeroInteractions(this.jdbcOperations);
verifyNoMoreInteractions(this.jdbcOperations);
}
@Test
@@ -533,10 +499,9 @@ class JdbcOperationsSessionRepositoryTests {
given(this.jdbcOperations.query(isA(String.class), isA(PreparedStatementSetter.class),
isA(ResultSetExtractor.class))).willReturn(Collections.emptyList());
JdbcOperationsSessionRepository.JdbcSession session = this.repository.findById(sessionId);
JdbcSession session = this.repository.findById(sessionId);
assertThat(session).isNull();
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).query(isA(String.class), isA(PreparedStatementSetter.class),
isA(ResultSetExtractor.class));
}
@@ -549,10 +514,9 @@ class JdbcOperationsSessionRepositoryTests {
given(this.jdbcOperations.query(isA(String.class), isA(PreparedStatementSetter.class),
isA(ResultSetExtractor.class))).willReturn(Collections.singletonList(expired));
JdbcOperationsSessionRepository.JdbcSession session = this.repository.findById(expired.getId());
JdbcSession session = this.repository.findById(expired.getId());
assertThat(session).isNull();
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).query(isA(String.class), isA(PreparedStatementSetter.class),
isA(ResultSetExtractor.class));
verify(this.jdbcOperations, times(1)).update(startsWith("DELETE"), eq(expired.getId()));
@@ -566,12 +530,11 @@ class JdbcOperationsSessionRepositoryTests {
given(this.jdbcOperations.query(isA(String.class), isA(PreparedStatementSetter.class),
isA(ResultSetExtractor.class))).willReturn(Collections.singletonList(saved));
JdbcOperationsSessionRepository.JdbcSession session = this.repository.findById(saved.getId());
JdbcSession session = this.repository.findById(saved.getId());
assertThat(session.getId()).isEqualTo(saved.getId());
assertThat(session.isNew()).isFalse();
assertThat(session.<String>getAttribute("savedName")).isEqualTo("savedValue");
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).query(isA(String.class), isA(PreparedStatementSetter.class),
isA(ResultSetExtractor.class));
}
@@ -582,7 +545,6 @@ class JdbcOperationsSessionRepositoryTests {
this.repository.deleteById(sessionId);
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).update(startsWith("DELETE"), eq(sessionId));
}
@@ -590,11 +552,10 @@ class JdbcOperationsSessionRepositoryTests {
void findByIndexNameAndIndexValueUnknownIndexName() {
String indexValue = "testIndexValue";
Map<String, JdbcOperationsSessionRepository.JdbcSession> sessions = this.repository
.findByIndexNameAndIndexValue("testIndexName", indexValue);
Map<String, JdbcSession> sessions = this.repository.findByIndexNameAndIndexValue("testIndexName", indexValue);
assertThat(sessions).isEmpty();
verifyZeroInteractions(this.jdbcOperations);
verifyNoMoreInteractions(this.jdbcOperations);
}
@Test
@@ -604,11 +565,10 @@ class JdbcOperationsSessionRepositoryTests {
given(this.jdbcOperations.query(isA(String.class), isA(PreparedStatementSetter.class),
isA(ResultSetExtractor.class))).willReturn(Collections.emptyList());
Map<String, JdbcOperationsSessionRepository.JdbcSession> sessions = this.repository
Map<String, JdbcSession> sessions = this.repository
.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principal);
assertThat(sessions).isEmpty();
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).query(isA(String.class), isA(PreparedStatementSetter.class),
isA(ResultSetExtractor.class));
}
@@ -629,11 +589,10 @@ class JdbcOperationsSessionRepositoryTests {
given(this.jdbcOperations.query(isA(String.class), isA(PreparedStatementSetter.class),
isA(ResultSetExtractor.class))).willReturn(saved);
Map<String, JdbcOperationsSessionRepository.JdbcSession> sessions = this.repository
Map<String, JdbcSession> sessions = this.repository
.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principal);
assertThat(sessions).hasSize(2);
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).query(isA(String.class), isA(PreparedStatementSetter.class),
isA(ResultSetExtractor.class));
}
@@ -642,13 +601,12 @@ class JdbcOperationsSessionRepositoryTests {
void cleanupExpiredSessions() {
this.repository.cleanUpExpiredSessions();
assertPropagationRequiresNew();
verify(this.jdbcOperations, times(1)).update(startsWith("DELETE"), anyLong());
}
@Test // gh-1120
void getAttributeNamesAndRemove() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
JdbcSession session = this.repository.createSession();
session.setAttribute("attribute1", "value1");
session.setAttribute("attribute2", "value2");
@@ -659,84 +617,6 @@ class JdbcOperationsSessionRepositoryTests {
assertThat(session.getAttributeNames()).isEmpty();
}
@Test
void saveNewWithoutTransaction() {
this.repository = new JdbcOperationsSessionRepository(this.jdbcOperations);
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
this.repository.save(session);
verify(this.jdbcOperations, times(1)).update(startsWith("INSERT INTO SPRING_SESSION"),
isA(PreparedStatementSetter.class));
verifyZeroInteractions(this.jdbcOperations);
verifyZeroInteractions(this.transactionManager);
}
@Test
void saveUpdatedWithoutTransaction() {
this.repository = new JdbcOperationsSessionRepository(this.jdbcOperations);
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
"primaryKey", false);
session.setLastAccessedTime(Instant.now());
this.repository.save(session);
verify(this.jdbcOperations, times(1)).update(startsWith("UPDATE SPRING_SESSION"),
isA(PreparedStatementSetter.class));
verifyZeroInteractions(this.jdbcOperations);
verifyZeroInteractions(this.transactionManager);
}
@Test
@SuppressWarnings("unchecked")
void findByIdWithoutTransaction() {
given(this.jdbcOperations.query(anyString(), any(PreparedStatementSetter.class), any(ResultSetExtractor.class)))
.willReturn(Collections.emptyList());
this.repository = new JdbcOperationsSessionRepository(this.jdbcOperations);
this.repository.findById("testSessionId");
verify(this.jdbcOperations, times(1)).query(endsWith("WHERE S.SESSION_ID = ?"),
isA(PreparedStatementSetter.class), isA(ResultSetExtractor.class));
verifyZeroInteractions(this.jdbcOperations);
verifyZeroInteractions(this.transactionManager);
}
@Test
void deleteByIdWithoutTransaction() {
this.repository = new JdbcOperationsSessionRepository(this.jdbcOperations);
this.repository.deleteById("testSessionId");
verify(this.jdbcOperations, times(1)).update(eq("DELETE FROM SPRING_SESSION WHERE SESSION_ID = ?"),
anyString());
verifyZeroInteractions(this.jdbcOperations);
verifyZeroInteractions(this.transactionManager);
}
@Test
@SuppressWarnings("unchecked")
void findByIndexNameAndIndexValueWithoutTransaction() {
given(this.jdbcOperations.query(anyString(), any(PreparedStatementSetter.class), any(ResultSetExtractor.class)))
.willReturn(Collections.emptyList());
this.repository = new JdbcOperationsSessionRepository(this.jdbcOperations);
this.repository.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME,
"testIndexValue");
verify(this.jdbcOperations, times(1)).query(endsWith("WHERE S.PRINCIPAL_NAME = ?"),
isA(PreparedStatementSetter.class), isA(ResultSetExtractor.class));
verifyZeroInteractions(this.jdbcOperations);
verifyZeroInteractions(this.transactionManager);
}
@Test
void cleanUpExpiredSessionsWithoutTransaction() {
this.repository = new JdbcOperationsSessionRepository(this.jdbcOperations);
this.repository.cleanUpExpiredSessions();
verify(this.jdbcOperations, times(1)).update(eq("DELETE FROM SPRING_SESSION WHERE EXPIRY_TIME < ?"), anyLong());
verifyZeroInteractions(this.jdbcOperations);
verifyZeroInteractions(this.transactionManager);
}
@Test
void saveWithSaveModeOnSetAttribute() {
this.repository.setSaveMode(SaveMode.ON_SET_ATTRIBUTE);
@@ -744,14 +624,13 @@ class JdbcOperationsSessionRepositoryTests {
delegate.setAttribute("attribute1", (Supplier<String>) () -> "value1");
delegate.setAttribute("attribute2", (Supplier<String>) () -> "value2");
delegate.setAttribute("attribute3", (Supplier<String>) () -> "value3");
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(delegate,
UUID.randomUUID().toString(), false);
JdbcSession session = this.repository.new JdbcSession(delegate, UUID.randomUUID().toString(), false);
session.getAttribute("attribute2");
session.setAttribute("attribute3", "value4");
this.repository.save(session);
verify(this.jdbcOperations).update(startsWith("UPDATE SPRING_SESSION_ATTRIBUTES SET"),
isA(PreparedStatementSetter.class));
verifyZeroInteractions(this.jdbcOperations);
verifyNoMoreInteractions(this.jdbcOperations);
}
@Test
@@ -761,8 +640,7 @@ class JdbcOperationsSessionRepositoryTests {
delegate.setAttribute("attribute1", (Supplier<String>) () -> "value1");
delegate.setAttribute("attribute2", (Supplier<String>) () -> "value2");
delegate.setAttribute("attribute3", (Supplier<String>) () -> "value3");
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(delegate,
UUID.randomUUID().toString(), false);
JdbcSession session = this.repository.new JdbcSession(delegate, UUID.randomUUID().toString(), false);
session.getAttribute("attribute2");
session.setAttribute("attribute3", "value4");
this.repository.save(session);
@@ -770,7 +648,7 @@ class JdbcOperationsSessionRepositoryTests {
.forClass(BatchPreparedStatementSetter.class);
verify(this.jdbcOperations).batchUpdate(startsWith("UPDATE SPRING_SESSION_ATTRIBUTES SET"), captor.capture());
assertThat(captor.getValue().getBatchSize()).isEqualTo(2);
verifyZeroInteractions(this.jdbcOperations);
verifyNoMoreInteractions(this.jdbcOperations);
}
@Test
@@ -780,8 +658,7 @@ class JdbcOperationsSessionRepositoryTests {
delegate.setAttribute("attribute1", (Supplier<String>) () -> "value1");
delegate.setAttribute("attribute2", (Supplier<String>) () -> "value2");
delegate.setAttribute("attribute3", (Supplier<String>) () -> "value3");
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(delegate,
UUID.randomUUID().toString(), false);
JdbcSession session = this.repository.new JdbcSession(delegate, UUID.randomUUID().toString(), false);
session.getAttribute("attribute2");
session.setAttribute("attribute3", "value4");
this.repository.save(session);
@@ -789,7 +666,7 @@ class JdbcOperationsSessionRepositoryTests {
.forClass(BatchPreparedStatementSetter.class);
verify(this.jdbcOperations).batchUpdate(startsWith("UPDATE SPRING_SESSION_ATTRIBUTES SET"), captor.capture());
assertThat(captor.getValue().getBatchSize()).isEqualTo(3);
verifyZeroInteractions(this.jdbcOperations);
verifyNoMoreInteractions(this.jdbcOperations);
}
@Test
@@ -798,7 +675,6 @@ class JdbcOperationsSessionRepositoryTests {
JdbcSession session = this.repository.new JdbcSession(new MapSession(), "primaryKey", false);
String attrName = "someAttribute";
session.setAttribute(attrName, "someValue");
assertPropagationRequiresNew();
verify(this.jdbcOperations).update(startsWith("INSERT INTO SPRING_SESSION_ATTRIBUTES("),
isA(PreparedStatementSetter.class));
verifyNoMoreInteractions(this.jdbcOperations);
@@ -811,7 +687,6 @@ class JdbcOperationsSessionRepositoryTests {
cached.setAttribute("attribute1", "value1");
JdbcSession session = this.repository.new JdbcSession(cached, "primaryKey", false);
session.removeAttribute("attribute1");
assertPropagationRequiresNew();
verify(this.jdbcOperations).update(startsWith("DELETE FROM SPRING_SESSION_ATTRIBUTES WHERE"),
isA(PreparedStatementSetter.class));
verifyNoMoreInteractions(this.jdbcOperations);
@@ -822,7 +697,6 @@ class JdbcOperationsSessionRepositoryTests {
this.repository.setFlushMode(FlushMode.IMMEDIATE);
JdbcSession session = this.repository.new JdbcSession(new MapSession(), "primaryKey", false);
session.setMaxInactiveInterval(Duration.ofSeconds(1));
assertPropagationRequiresNew();
verify(this.jdbcOperations).update(startsWith("UPDATE SPRING_SESSION SET"), isA(PreparedStatementSetter.class));
verifyNoMoreInteractions(this.jdbcOperations);
}
@@ -832,16 +706,8 @@ class JdbcOperationsSessionRepositoryTests {
this.repository.setFlushMode(FlushMode.IMMEDIATE);
JdbcSession session = this.repository.new JdbcSession(new MapSession(), "primaryKey", false);
session.setLastAccessedTime(Instant.now());
assertPropagationRequiresNew();
verify(this.jdbcOperations).update(startsWith("UPDATE SPRING_SESSION SET"), isA(PreparedStatementSetter.class));
verifyNoMoreInteractions(this.jdbcOperations);
}
private void assertPropagationRequiresNew() {
ArgumentCaptor<TransactionDefinition> argument = ArgumentCaptor.forClass(TransactionDefinition.class);
verify(this.transactionManager, atLeastOnce()).getTransaction(argument.capture());
assertThat(argument.getValue().getPropagationBehavior())
.isEqualTo(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
}
}

View File

@@ -27,16 +27,22 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.annotation.Order;
import org.springframework.core.convert.ConversionService;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.support.lob.LobHandler;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.session.FlushMode;
import org.springframework.session.IndexResolver;
import org.springframework.session.SaveMode;
import org.springframework.session.jdbc.JdbcOperationsSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.config.SessionRepositoryCustomizer;
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
import org.springframework.session.jdbc.config.annotation.SpringSessionDataSource;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionOperations;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -47,7 +53,6 @@ import static org.mockito.Mockito.mock;
*
* @author Vedran Pavic
* @author Eddú Meléndez
* @since 1.2.0
*/
class JdbcHttpSessionConfigurationTests {
@@ -77,14 +82,17 @@ class JdbcHttpSessionConfigurationTests {
void defaultConfiguration() {
registerAndRefresh(DataSourceConfiguration.class, DefaultConfiguration.class);
assertThat(this.context.getBean(JdbcOperationsSessionRepository.class)).isNotNull();
JdbcIndexedSessionRepository sessionRepository = this.context.getBean(JdbcIndexedSessionRepository.class);
assertThat(sessionRepository).isNotNull();
assertThat(sessionRepository).extracting("transactionOperations")
.hasFieldOrPropertyWithValue("propagationBehavior", TransactionDefinition.PROPAGATION_REQUIRES_NEW);
}
@Test
void customTableNameAnnotation() {
registerAndRefresh(DataSourceConfiguration.class, CustomTableNameAnnotationConfiguration.class);
JdbcOperationsSessionRepository repository = this.context.getBean(JdbcOperationsSessionRepository.class);
JdbcIndexedSessionRepository repository = this.context.getBean(JdbcIndexedSessionRepository.class);
assertThat(repository).isNotNull();
assertThat(ReflectionTestUtils.getField(repository, "tableName")).isEqualTo(TABLE_NAME);
}
@@ -93,7 +101,7 @@ class JdbcHttpSessionConfigurationTests {
void customTableNameSetter() {
registerAndRefresh(DataSourceConfiguration.class, CustomTableNameSetterConfiguration.class);
JdbcOperationsSessionRepository repository = this.context.getBean(JdbcOperationsSessionRepository.class);
JdbcIndexedSessionRepository repository = this.context.getBean(JdbcIndexedSessionRepository.class);
assertThat(repository).isNotNull();
assertThat(ReflectionTestUtils.getField(repository, "tableName")).isEqualTo(TABLE_NAME);
}
@@ -103,7 +111,7 @@ class JdbcHttpSessionConfigurationTests {
registerAndRefresh(DataSourceConfiguration.class,
CustomMaxInactiveIntervalInSecondsAnnotationConfiguration.class);
JdbcOperationsSessionRepository repository = this.context.getBean(JdbcOperationsSessionRepository.class);
JdbcIndexedSessionRepository repository = this.context.getBean(JdbcIndexedSessionRepository.class);
assertThat(repository).isNotNull();
assertThat(ReflectionTestUtils.getField(repository, "defaultMaxInactiveInterval"))
.isEqualTo(MAX_INACTIVE_INTERVAL_IN_SECONDS);
@@ -113,7 +121,7 @@ class JdbcHttpSessionConfigurationTests {
void customMaxInactiveIntervalInSecondsSetter() {
registerAndRefresh(DataSourceConfiguration.class, CustomMaxInactiveIntervalInSecondsSetterConfiguration.class);
JdbcOperationsSessionRepository repository = this.context.getBean(JdbcOperationsSessionRepository.class);
JdbcIndexedSessionRepository repository = this.context.getBean(JdbcIndexedSessionRepository.class);
assertThat(repository).isNotNull();
assertThat(ReflectionTestUtils.getField(repository, "defaultMaxInactiveInterval"))
.isEqualTo(MAX_INACTIVE_INTERVAL_IN_SECONDS);
@@ -169,7 +177,7 @@ class JdbcHttpSessionConfigurationTests {
void qualifiedDataSourceConfiguration() {
registerAndRefresh(DataSourceConfiguration.class, QualifiedDataSourceConfiguration.class);
JdbcOperationsSessionRepository repository = this.context.getBean(JdbcOperationsSessionRepository.class);
JdbcIndexedSessionRepository repository = this.context.getBean(JdbcIndexedSessionRepository.class);
DataSource dataSource = this.context.getBean("qualifiedDataSource", DataSource.class);
assertThat(repository).isNotNull();
assertThat(dataSource).isNotNull();
@@ -182,7 +190,7 @@ class JdbcHttpSessionConfigurationTests {
void primaryDataSourceConfiguration() {
registerAndRefresh(DataSourceConfiguration.class, PrimaryDataSourceConfiguration.class);
JdbcOperationsSessionRepository repository = this.context.getBean(JdbcOperationsSessionRepository.class);
JdbcIndexedSessionRepository repository = this.context.getBean(JdbcIndexedSessionRepository.class);
DataSource dataSource = this.context.getBean("primaryDataSource", DataSource.class);
assertThat(repository).isNotNull();
assertThat(dataSource).isNotNull();
@@ -195,7 +203,7 @@ class JdbcHttpSessionConfigurationTests {
void qualifiedAndPrimaryDataSourceConfiguration() {
registerAndRefresh(DataSourceConfiguration.class, QualifiedAndPrimaryDataSourceConfiguration.class);
JdbcOperationsSessionRepository repository = this.context.getBean(JdbcOperationsSessionRepository.class);
JdbcIndexedSessionRepository repository = this.context.getBean(JdbcIndexedSessionRepository.class);
DataSource dataSource = this.context.getBean("qualifiedDataSource", DataSource.class);
assertThat(repository).isNotNull();
assertThat(dataSource).isNotNull();
@@ -208,7 +216,7 @@ class JdbcHttpSessionConfigurationTests {
void namedDataSourceConfiguration() {
registerAndRefresh(DataSourceConfiguration.class, NamedDataSourceConfiguration.class);
JdbcOperationsSessionRepository repository = this.context.getBean(JdbcOperationsSessionRepository.class);
JdbcIndexedSessionRepository repository = this.context.getBean(JdbcIndexedSessionRepository.class);
DataSource dataSource = this.context.getBean("dataSource", DataSource.class);
assertThat(repository).isNotNull();
assertThat(dataSource).isNotNull();
@@ -225,11 +233,33 @@ class JdbcHttpSessionConfigurationTests {
.withMessageContaining("expected single matching bean but found 2");
}
@Test
void customTransactionOperationsConfiguration() {
registerAndRefresh(DataSourceConfiguration.class, CustomTransactionOperationsConfiguration.class);
JdbcIndexedSessionRepository repository = this.context.getBean(JdbcIndexedSessionRepository.class);
TransactionOperations transactionOperations = this.context.getBean(TransactionOperations.class);
assertThat(repository).isNotNull();
assertThat(transactionOperations).isNotNull();
assertThat(repository).hasFieldOrPropertyWithValue("transactionOperations", transactionOperations);
}
@Test
void customIndexResolverConfiguration() {
registerAndRefresh(DataSourceConfiguration.class, CustomIndexResolverConfiguration.class);
JdbcIndexedSessionRepository repository = this.context.getBean(JdbcIndexedSessionRepository.class);
@SuppressWarnings("unchecked")
IndexResolver<Session> indexResolver = this.context.getBean(IndexResolver.class);
assertThat(repository).isNotNull();
assertThat(indexResolver).isNotNull();
assertThat(repository).hasFieldOrPropertyWithValue("indexResolver", indexResolver);
}
@Test
void customLobHandlerConfiguration() {
registerAndRefresh(DataSourceConfiguration.class, CustomLobHandlerConfiguration.class);
JdbcOperationsSessionRepository repository = this.context.getBean(JdbcOperationsSessionRepository.class);
JdbcIndexedSessionRepository repository = this.context.getBean(JdbcIndexedSessionRepository.class);
LobHandler lobHandler = this.context.getBean(LobHandler.class);
assertThat(repository).isNotNull();
assertThat(lobHandler).isNotNull();
@@ -240,7 +270,7 @@ class JdbcHttpSessionConfigurationTests {
void customConversionServiceConfiguration() {
registerAndRefresh(DataSourceConfiguration.class, CustomConversionServiceConfiguration.class);
JdbcOperationsSessionRepository repository = this.context.getBean(JdbcOperationsSessionRepository.class);
JdbcIndexedSessionRepository repository = this.context.getBean(JdbcIndexedSessionRepository.class);
ConversionService conversionService = this.context.getBean("springSessionConversionService",
ConversionService.class);
assertThat(repository).isNotNull();
@@ -258,6 +288,14 @@ class JdbcHttpSessionConfigurationTests {
assertThat(ReflectionTestUtils.getField(configuration, "tableName")).isEqualTo("custom_session_table");
}
@Test
void sessionRepositoryCustomizer() {
registerAndRefresh(DataSourceConfiguration.class, SessionRepositoryCustomizerConfiguration.class);
JdbcIndexedSessionRepository sessionRepository = this.context.getBean(JdbcIndexedSessionRepository.class);
assertThat(sessionRepository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval",
MAX_INACTIVE_INTERVAL_IN_SECONDS);
}
private void registerAndRefresh(Class<?>... annotatedClasses) {
this.context.register(annotatedClasses);
this.context.refresh();
@@ -272,12 +310,12 @@ class JdbcHttpSessionConfigurationTests {
static class DataSourceConfiguration {
@Bean
public DataSource defaultDataSource() {
DataSource defaultDataSource() {
return mock(DataSource.class);
}
@Bean
public PlatformTransactionManager transactionManager() {
PlatformTransactionManager transactionManager() {
return mock(PlatformTransactionManager.class);
}
@@ -363,7 +401,7 @@ class JdbcHttpSessionConfigurationTests {
@Bean
@SpringSessionDataSource
public DataSource qualifiedDataSource() {
DataSource qualifiedDataSource() {
return mock(DataSource.class);
}
@@ -374,7 +412,7 @@ class JdbcHttpSessionConfigurationTests {
@Bean
@Primary
public DataSource primaryDataSource() {
DataSource primaryDataSource() {
return mock(DataSource.class);
}
@@ -385,13 +423,13 @@ class JdbcHttpSessionConfigurationTests {
@Bean
@SpringSessionDataSource
public DataSource qualifiedDataSource() {
DataSource qualifiedDataSource() {
return mock(DataSource.class);
}
@Bean
@Primary
public DataSource primaryDataSource() {
DataSource primaryDataSource() {
return mock(DataSource.class);
}
@@ -401,7 +439,7 @@ class JdbcHttpSessionConfigurationTests {
static class NamedDataSourceConfiguration {
@Bean
public DataSource dataSource() {
DataSource dataSource() {
return mock(DataSource.class);
}
@@ -411,17 +449,38 @@ class JdbcHttpSessionConfigurationTests {
static class MultipleDataSourceConfiguration {
@Bean
public DataSource secondaryDataSource() {
DataSource secondaryDataSource() {
return mock(DataSource.class);
}
}
@EnableJdbcHttpSession
static class CustomTransactionOperationsConfiguration {
@Bean
TransactionOperations springSessionTransactionOperations() {
return TransactionOperations.withoutTransaction();
}
}
@EnableJdbcHttpSession
static class CustomIndexResolverConfiguration {
@Bean
@SuppressWarnings("unchecked")
IndexResolver<Session> indexResolver() {
return mock(IndexResolver.class);
}
}
@EnableJdbcHttpSession
static class CustomLobHandlerConfiguration {
@Bean
public LobHandler springSessionLobHandler() {
LobHandler springSessionLobHandler() {
return mock(LobHandler.class);
}
@@ -431,7 +490,7 @@ class JdbcHttpSessionConfigurationTests {
static class CustomConversionServiceConfiguration {
@Bean
public ConversionService springSessionConversionService() {
ConversionService springSessionConversionService() {
return mock(ConversionService.class);
}
@@ -441,10 +500,28 @@ class JdbcHttpSessionConfigurationTests {
static class CustomJdbcHttpSessionConfiguration {
@Bean
public PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
@EnableJdbcHttpSession
static class SessionRepositoryCustomizerConfiguration {
@Bean
@Order(0)
SessionRepositoryCustomizer<JdbcIndexedSessionRepository> sessionRepositoryCustomizerOne() {
return (sessionRepository) -> sessionRepository.setDefaultMaxInactiveInterval(0);
}
@Bean
@Order(1)
SessionRepositoryCustomizer<JdbcIndexedSessionRepository> sessionRepositoryCustomizerTwo() {
return (sessionRepository) -> sessionRepository
.setDefaultMaxInactiveInterval(MAX_INACTIVE_INTERVAL_IN_SECONDS);
}
}
}

View File

@@ -1,15 +1,24 @@
ext['spring-framework.version'] = '5.2.0.RC1'
ext['jackson.version'] = '2.10.0'
ext['reactor-bom.version'] = 'Dysprosium-RELEASE'
ext['spring-data-releasetrain.version'] = 'Moore-RELEASE'
ext['spring-framework.version'] = '5.2.0.RELEASE'
ext['spring-security.version'] = '5.2.0.RELEASE'
ext['webjars-locator-core.version'] = '0.38'
dependencyManagement {
imports {
mavenBom 'com.fasterxml.jackson:jackson-bom:2.10.0'
}
dependencies {
dependency 'ch.qos.logback:logback-classic:1.2.3'
dependency 'com.maxmind.geoip2:geoip2:2.3.1'
dependency 'javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api:1.2.1'
dependency 'javax.servlet.jsp:javax.servlet.jsp-api:2.3.2-b02'
dependency 'javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api:1.2.2'
dependency 'javax.servlet.jsp:javax.servlet.jsp-api:2.3.3'
dependency 'org.apache.taglibs:taglibs-standard-jstlel:1.2.5'
dependency 'org.seleniumhq.selenium:htmlunit-driver:2.33.0'
dependency 'org.slf4j:jcl-over-slf4j:1.7.25'
dependency 'org.slf4j:log4j-over-slf4j:1.7.25'
dependency 'org.slf4j:jcl-over-slf4j:1.7.28'
dependency 'org.slf4j:log4j-over-slf4j:1.7.28'
dependency 'org.webjars:bootstrap:2.3.2'
dependency 'org.webjars:html5shiv:3.7.3'
dependency 'org.webjars:jquery:1.12.4'

Some files were not shown because too many files have changed in this diff Show More