Compare commits

..

74 Commits

Author SHA1 Message Date
Greg L. Turnquist
7ef121e0fd Replace Flapdoodle with Testcontainers for MongoDB support.
For more details on this usage of Testcontainers, see https://bsideup.github.io/posts/local_development_with_testcontainers/

Related issues: https://github.com/spring-projects/spring-boot/issues/30863
2022-05-13 16:34:25 -05:00
Eleftheria Stein
aaed973d27 Add Caffeine community extension
Closes gh-2039
2022-04-27 09:50:11 +02:00
Eleftheria Stein
55c4fcfd3f Fix link to Infinispan cache 2022-04-27 09:49:41 +02:00
Eleftheria Stein
2a5d6b4d2e Next development version 2022-04-19 18:39:03 +02:00
Eleftheria Stein
42aa10bfe8 Release 2.7.0-RC1 2022-04-19 18:20:16 +02:00
Eleftheria Stein
221b48094a Upgrade test dependencies 2022-04-19 17:59:17 +02:00
Eleftheria Stein
d43b48bbb0 Revert "Upgrade test dependencies"
This reverts commit 2369b2cfb3.
2022-04-19 17:57:52 +02:00
Eleftheria Stein
e1dea5e0a8 Revise Redis test mocks based on new Data implementation
Issue gh-2070
2022-04-19 15:53:23 +02:00
Eleftheria Stein
23a32acd56 Upgrade Spring Data to 2021.2.0-RC1
Closes gh-2059
2022-04-19 15:18:55 +02:00
Eleftheria Stein
dc8cca5351 Upgrade Spring Security to 5.7.0-RC1
Closes gh-2070
2022-04-19 13:19:15 +02:00
Eleftheria Stein
2369b2cfb3 Upgrade test dependencies 2022-04-19 13:19:10 +02:00
Eleftheria Stein
e6ce56ec8b Upgrade Reactor to 2020.0.18
Closes gh-2071
2022-04-19 13:17:57 +02:00
Eleftheria Stein
36939c1b02 Upgrade Jackson to 2.13.2.20220328
Closes gh-2068
2022-04-19 13:17:25 +02:00
Eleftheria Stein
0a84f9d544 Upgrade Spring Framework to 5.3.19
Closes gh-2069
2022-04-19 13:15:32 +02:00
Eleftheria Stein
a5ec1ccf1f Fix formatting 2022-04-19 11:13:49 +02:00
Greg L. Turnquist
7fc0ae47d5 Switch back to unicode for the DOT substitute character.
MongoDB doesn't support "." in field names, so a Private Use Area character was used. This was originally stored in unicode format, but delomboking the code caused it to get transformed into another encoding. This causes issues on certain systems when building the software, so we are converting it back to its unicode representation. The character has been the same throughout, ensuring binary compatilibity.

See: https://www.compart.com/en/unicode/U+F607

Related: d601e270fc (diff-57190a47726099e31fdf86b12b80206e2ae24feb28aacaf494b99557583df150L47)
Closes #2053.
2022-04-18 09:49:57 -05:00
Jerome Prinet
ce2e644e04 Update Gradle Enterprise plugin to 3.9 2022-04-14 11:10:54 +02:00
Felix Scheinost
8800fb9816 Fix bug in JDBC SaveMode.ON_GET_ATTRIBUTE 2022-04-08 17:19:54 +02:00
Eleftheria Stein
b8e94948ae Upgrade MongoDB to 4.5.1
Closes gh-2047
2022-03-30 15:24:02 +02:00
Eleftheria Stein
8d2276341f Upgrade samples to Spring Boot 2.7.0-M3
This also makes the necessary changes for the Thymeleaf Layout Dialect 3.0 update.

Closes gh-2046
2022-03-30 15:23:49 +02:00
Eleftheria Stein
63f1c7bf6f Fix reference documentation redirects
Closes gh-2044
2022-03-23 10:03:32 +01:00
Eleftheria Stein
e8e4ee2850 Make RedisSessionRepository.DEFAULT_KEY_NAMESPACE public
Closes gh-2043
2022-03-15 18:23:17 +01:00
Eleftheria Stein
9a643c8866 Update to Antora 3.0.1
Closes gh-2038
2022-03-11 15:46:19 +01:00
Eleftheria Stein
282f774e07 Document release process for 2.7.x
Issue gh-2036
2022-02-24 12:46:05 +01:00
Eleftheria Stein
e6ec5765b8 Fix 2.6.2 reference docs
Closes gh-2035
2022-02-22 15:43:37 +01:00
Eleftheria Stein
ab5c727846 Update Websocket sample to be compatible with H2 2.0
Closes gh-2013
2022-01-27 14:11:26 +01:00
Eleftheria Stein
44ff959c59 Include 2.6.x branch in Antora docs
Issue gh-2014
2022-01-21 10:13:52 +01:00
Eleftheria Stein
ad67a3775b Add manual trigger to antora generate workflow
Issue gh-2014
2022-01-20 13:52:11 +01:00
Eleftheria Stein
75c60b27bd Exclude 3.0.0-M1 and 2.7.0-M1 tags from antora
Closes gh-2014
2022-01-20 13:22:31 +01:00
Ruslan Molchanov
81bd6bd261 Fix memory leak with null principal in Redis
Closes gh-1987
2022-01-19 17:31:35 +01:00
Eleftheria Stein
2e8c4292fd Next development version 2022-01-18 13:01:30 +01:00
Eleftheria Stein
315b9c9929 Release 2.7.0-M1 2022-01-18 12:51:40 +01:00
Eleftheria Stein
a70abd90bd Upgrade test dependencies 2022-01-18 11:29:58 +01:00
Eleftheria Stein
f13df5aa2f Revert "Upgrade test dependencies"
This reverts commit bda72c074f.
2022-01-18 11:14:02 +01:00
Eleftheria Stein
6fd68e093f Upgrade Spring Data to 2021.2.0-M2
Closes gh-1996
2022-01-18 10:47:16 +01:00
Eleftheria Stein
653d820290 Update antora tags to include 3.0 2022-01-18 10:35:06 +01:00
Jerome Prinet
950ac50234 Bump up Gradle plugin dependencies 2022-01-17 17:05:36 +01:00
Eleftheria Stein
bda72c074f Upgrade test dependencies 2022-01-17 14:51:48 +01:00
Eleftheria Stein
fb1362aa2c Upgrade Hazelcast 4 to 4.2.4
Closes gh-2001
2022-01-17 14:51:48 +01:00
Eleftheria Stein
d5dac6629d Upgrade Spring Security to 5.7.0-M1
Closes gh-1997
2022-01-17 14:51:45 +01:00
Eleftheria Stein
04b4b9ba17 Upgrade Spring Framework to 5.3.15
Closes gh-1998
2022-01-17 14:51:18 +01:00
Eleftheria Stein
97308bdbf4 Upgrade Jackson to 2.13.1
Closes gh-2002
2022-01-17 14:50:38 +01:00
Eleftheria Stein
4ba62c71dd Upgrade MongoDB to 4.4.1
Closes gh-2000
2022-01-17 14:50:38 +01:00
Eleftheria Stein
265099c586 Upgrade Reactor to 2020.0.15
Closes gh-1999
2022-01-17 09:41:09 +01:00
Eleftheria Stein
2f4a0110ab Update to jakarta.servlet-api dependency
Closes gh-1960
2022-01-14 16:21:35 +01:00
Eleftheria Stein
7a1cb66dae Update to jakarta.annotation-api dependency
Closes gh-1956
2022-01-14 16:19:22 +01:00
Jerome Prinet
9bad2afa14 Bump up Gradle enterprise plugin to 3.7.2 2022-01-05 14:34:51 +01:00
Eleftheria Stein
c79173879f Run CI on all branches 2022-01-05 14:29:28 +02:00
Eleftheria Stein
2c065c0241 Next minor development version 2022-01-05 14:28:19 +02:00
Eleftheria Stein
a08c721118 Next development version 2021-12-21 16:31:16 +02:00
Eleftheria Stein
70e0c6d22d Update antora version 2021-12-21 15:44:58 +02:00
Eleftheria Stein
61bf2eca49 Release 2.6.1 2021-12-21 15:12:26 +02:00
Eleftheria Stein
59923121f0 Make gretty samples compatible with logback 1.2.9
Explicitly reference the logback.xml file to prevent gretty from configuring the defaults using groovy.
2021-12-21 11:10:33 +02:00
Eleftheria Stein
b5f8e29585 Upgrade test dependencies 2021-12-21 11:10:22 +02:00
Eleftheria Stein
307a1f0dde Upgrade Hazelcast 4 to 4.2.3
Closes gh-1979
2021-12-21 10:09:06 +02:00
Eleftheria Stein
ced2d8421c Upgrade Spring Security to 5.6.1
Closes gh-1978
2021-12-21 10:08:25 +02:00
Eleftheria Stein
d98ff97e1a Upgrade Spring Framework to 5.3.14
Closes gh-1977
2021-12-21 10:03:02 +02:00
Eleftheria Stein
757175516f Upgrade Reactor to 2020.0.14
Closes gh-1976
2021-12-21 10:02:30 +02:00
Eleftheria Stein
8ae55b7ee4 GitHub Actions uses spring-builds+github user
This is more clear than spring-builds user
2021-12-07 15:43:42 +01:00
Eleftheria Stein
1eb53ead9d Update README to include MongoDB
Issue gh-1901
2021-11-26 17:37:12 +01:00
Guillaume Husta
72159794f4 Doc : typo on Username _user-
user should be in italic
2021-11-19 14:56:56 +01:00
Eleftheria Stein
aaf122f3a6 Include 2.6.0 and 3.0 snapshots in reference docs 2021-11-17 11:28:54 +01:00
Eleftheria Stein
6f823805f2 Include 2.6.8 release in docs 2021-11-17 11:04:55 +01:00
Eleftheria Stein
7807aa9f3c Next development version 2021-11-16 18:56:49 +01:00
Eleftheria Stein
76924bc923 Release 2.6.0 2021-11-16 14:21:51 +01:00
Eleftheria Stein
134f89dd41 Upgrade test dependencies 2021-11-16 13:57:45 +01:00
Eleftheria Stein
33812f7197 Upgrade MongoDB to 4.4.0
Closes gh-1967
2021-11-16 13:56:44 +01:00
Eleftheria Stein
bcf17ba3b7 Upgrade samples to Spring Boot 2.5.6
Closes gh-1966
2021-11-16 13:37:41 +01:00
Eleftheria Stein
98f656ad46 Upgrade Jackson to 2.13.0
Closes gh-1965
2021-11-16 13:11:51 +01:00
Eleftheria Stein
7832942752 Upgrade Spring Security to 5.6.0
Closes gh-1964
2021-11-16 13:07:43 +01:00
Eleftheria Stein
722069a5f8 Upgrade Spring Data to 2021.1.0
Closes gh-1963
2021-11-16 13:06:41 +01:00
Eleftheria Stein
addbdbc1a2 Upgrade Spring Framework to 5.3.13
Closes gh-1962
2021-11-16 13:05:51 +01:00
Eleftheria Stein
004466ed07 Upgrade Reactor to 2020.0.13
Closes gh-1961
2021-11-16 13:04:32 +01:00
Eleftheria Stein
aeb5bc545c Update to Gradle 7.3
Closes gh-1959
2021-11-15 09:59:22 +01:00
196 changed files with 3616 additions and 1865 deletions

View File

@@ -1,6 +1,7 @@
name: Generate Antora Files and Request Build
on:
workflow_dispatch: # Manual trigger
push:
branches-ignore:
- 'gh-pages'
@@ -14,12 +15,6 @@ jobs:
steps:
- name: Checkout Source
uses: actions/checkout@v2
- name: Set up JDK 17
uses: actions/setup-java@v2
with:
java-version: '17'
distribution: 'adopt'
cache: gradle
- name: Generate antora.yml
run: ./gradlew :spring-session-docs:generateAntora
- name: Extract Branch Name

View File

@@ -20,7 +20,7 @@ jobs:
if: github.repository == 'spring-projects/spring-session'
strategy:
matrix:
jdk: [17]
jdk: [8, 11]
fail-fast: false
steps:
- uses: actions/checkout@v2
@@ -53,7 +53,7 @@ jobs:
- name: Set up JDK
uses: actions/setup-java@v1
with:
java-version: '17'
java-version: '8'
- name: Setup gradle user name
run: |
mkdir -p ~/.gradle
@@ -81,7 +81,7 @@ jobs:
- name: Set up JDK
uses: actions/setup-java@v1
with:
java-version: '17'
java-version: '8'
- name: Setup gradle user name
run: |
mkdir -p ~/.gradle

View File

@@ -13,10 +13,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 17
- name: Set up JDK 11
uses: actions/setup-java@v2
with:
java-version: '17'
java-version: '11'
distribution: 'adopt'
cache: gradle
- name: Validate Gradle wrapper

View File

@@ -9,7 +9,7 @@ jobs:
if: github.repository == 'spring-projects/spring-session'
strategy:
matrix:
jdk: [17]
jdk: [8, 11]
fail-fast: false
steps:
- uses: actions/checkout@v2

View File

@@ -1,6 +0,0 @@
# Use sdkman to run "sdk env" to initialize with correct JDK version
# Enable auto-env through the sdkman_auto_env config
# See https://sdkman.io/usage#config
# A summary is to add the following to ~/.sdkman/etc/config
# sdkman_auto_env=true
java=17.0.2-tem

View File

@@ -18,7 +18,7 @@ You can manually check at https://github.com/spring-projects/spring-session/mile
== 3. Update Release Version
Update the version number in `gradle.properties` for the release, for example `3.0.0-M1`, `3.0.0-RC1`, `3.0.4`
Update the version number in `gradle.properties` for the release, for example `2.7.0-M1`, `2.7.0-RC1`, `2.7.3`
== 4. Update Antora Version
@@ -26,9 +26,9 @@ You will need to update the antora.yml version.
For milestone / release candidate releases you should follow this format:
----
version: '3.0.0-RC1'
version: '2.7.0-RC1'
prerelease: 'true'
display_version: '3.0.0-RC1'
display_version: '2.7.0-RC1'
----
== 5. Build Locally
@@ -50,8 +50,8 @@ Wait for the artifact to appear in https://repo1.maven.org/maven2/org/springfram
Tag the release and then push the tag
....
git tag 3.0.0-RC1
git push origin 3.0.0-RC1
git tag 2.7.0-RC1
git push origin 2.7.0-RC1
....
== 8. Update to Next Development Version
@@ -76,7 +76,7 @@ java -jar github-changelog-generator.jar \
$MILESTONE release-notes
....
Note 1: `+$MILESTONE+` is something like `+3.0.4+` or `+3.0.0-M1+`. +
Note 1: `+$MILESTONE+` is something like `+2.7.1+` or `+2.7.0-M1+`. +
Note 2: This will create a file on your filesystem
called `+release-notes+`.

View File

@@ -4,7 +4,7 @@ buildscript {
snapshotBuild = version.endsWith('SNAPSHOT')
milestoneBuild = !(releaseBuild || snapshotBuild)
springBootVersion = '3.0.0-SNAPSHOT'
springBootVersion = '2.7.0-M3'
}
repositories {
@@ -35,7 +35,7 @@ subprojects {
apply plugin: 'io.spring.javaformat'
plugins.withType(JavaPlugin) {
sourceCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_1_8
}
tasks.withType(Test) {

View File

@@ -4,7 +4,7 @@ plugins {
id "groovy"
}
sourceCompatibility = JavaVersion.VERSION_11
sourceCompatibility = 1.8
repositories {
jcenter()
@@ -64,7 +64,7 @@ dependencies {
implementation 'com.github.spullara.mustache.java:compiler:0.9.10'
implementation 'io.spring.javaformat:spring-javaformat-gradle-plugin:0.0.15'
implementation 'io.spring.nohttp:nohttp-gradle:0.0.9'
implementation 'net.sourceforge.htmlunit:htmlunit:2.37.0'
implementation 'net.sourceforge.htmlunit:htmlunit:2.55.0'
implementation 'org.hidetake:gradle-ssh-plugin:2.10.1'
implementation 'org.jfrog.buildinfo:build-info-extractor-gradle:4.24.20'
implementation 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1'

Binary file not shown.

View File

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

View File

@@ -34,7 +34,7 @@ class JacocoPlugin implements Plugin<Project> {
project.tasks.check.dependsOn project.tasks.jacocoTestReport
project.jacoco {
toolVersion = '0.8.7'
toolVersion = '0.8.2'
}
}
}

View File

@@ -36,7 +36,7 @@ public class SpringSampleWarPlugin extends SpringSamplePlugin {
pluginManager.apply("org.gretty");
project.gretty {
servletContainer = 'tomcat10'
servletContainer = 'tomcat85'
contextPath = '/'
fileLogEnabled = false
}

View File

@@ -28,7 +28,7 @@ public class JavadocApiPluginITest {
.build();
assertThat(result.task(":api").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
File allClasses = new File(testKit.getRootDir(), "build/api/allclasses-noframe.html");
File index = new File(testKit.getRootDir(), "build/api/allclasses-index.html");
File index = new File(testKit.getRootDir(), "build/api/allclasses.html");
File listing = allClasses.exists() ? allClasses : index;
String listingText = FileUtils.readFileToString(listing);
assertThat(listingText).contains("sample/Api.html");

View File

@@ -9,7 +9,7 @@ repositories {
}
dependencies {
optional 'jakarta.servlet:jakarta.servlet-api:5.0.0'
optional 'jakarta.servlet:jakarta.servlet-api:4.0.4'
testImplementation platform('org.junit:junit-bom:5.8.1')
testImplementation 'org.junit.jupiter:junit-jupiter-api'
testImplementation 'org.junit.jupiter:junit-jupiter-engine'

View File

@@ -1,7 +1,7 @@
package sample;
import org.junit.jupiter.api.Test;
import jakarta.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequest;
public class TheTest {
@Test

View File

@@ -1,10 +1,10 @@
plugins {
id "org.gretty" version "4.0.0"
id "org.gretty" version "3.0.7"
id "io.spring.convention.spring-sample-war"
}
dependencies {
provided 'jakarta.servlet:jakarta.servlet-api:5.0.0'
provided 'jakarta.servlet:jakarta.servlet-api:4.0.4'
testImplementation 'commons-io:commons-io:2.11.0'
testImplementation 'org.assertj:assertj-core:3.21.0'
testImplementation platform('org.junit:junit-bom:5.8.1')

View File

@@ -18,11 +18,11 @@ package sample;
import java.io.IOException;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/")
public class HelloServlet extends HttpServlet {

View File

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

View File

@@ -1,45 +1,49 @@
dependencyManagement {
imports {
mavenBom 'io.projectreactor:reactor-bom:2020.0.19'
mavenBom 'com.fasterxml.jackson:jackson-bom:2.13.3'
mavenBom 'io.projectreactor:reactor-bom:2020.0.18'
mavenBom 'com.fasterxml.jackson:jackson-bom:2.13.2.20220328'
mavenBom 'org.junit:junit-bom:5.8.2'
mavenBom 'org.springframework:spring-framework-bom:6.0.0-M4'
mavenBom 'org.springframework.data:spring-data-bom:2022.0.0-M4'
mavenBom 'org.springframework.security:spring-security-bom:6.0.0-M5'
mavenBom 'org.testcontainers:testcontainers-bom:1.16.2'
mavenBom 'org.springframework:spring-framework-bom:5.3.19'
mavenBom 'org.springframework.data:spring-data-bom:2021.2.0-RC1'
mavenBom 'org.springframework.security:spring-security-bom:5.7.0-RC1'
mavenBom 'org.testcontainers:testcontainers-bom:1.17.1'
}
dependencies {
dependency 'com.hazelcast:hazelcast:5.0.2'
dependency 'org.aspectj:aspectjweaver:1.9.7'
dependency 'ch.qos.logback:logback-core:1.2.10'
dependencySet(group: 'com.hazelcast', version: '3.12.12') {
entry 'hazelcast'
entry 'hazelcast-client'
}
dependency 'org.aspectj:aspectjweaver:1.9.9.1'
dependency 'ch.qos.logback:logback-core:1.2.11'
dependency 'com.google.code.findbugs:jsr305:3.0.2'
dependency 'com.h2database:h2:2.1.212'
dependency 'com.ibm.db2:jcc:11.5.6.0'
dependency 'com.h2database:h2:1.4.200'
dependency 'com.ibm.db2:jcc:11.5.7.0'
dependency 'com.microsoft.sqlserver:mssql-jdbc:9.4.1.jre8'
dependency 'com.oracle.database.jdbc:ojdbc8:21.4.0.0.1'
dependency 'com.oracle.database.jdbc:ojdbc8:21.5.0.0'
dependency 'com.zaxxer:HikariCP:3.4.5'
dependency 'edu.umd.cs.mtc:multithreadedtc:1.01'
dependency 'io.lettuce:lettuce-core:6.1.6.RELEASE'
dependency 'jakarta.annotation:jakarta.annotation-api:2.0.0'
dependency 'jakarta.servlet:jakarta.servlet-api:5.0.0'
dependency 'mysql:mysql-connector-java:8.0.27'
dependency 'io.lettuce:lettuce-core:6.1.8.RELEASE'
dependency 'jakarta.annotation:jakarta.annotation-api:1.3.5'
dependency 'jakarta.servlet:jakarta.servlet-api:4.0.4'
dependency 'mysql:mysql-connector-java:8.0.28'
dependency 'org.apache.derby:derby:10.14.2.0'
dependency 'org.assertj:assertj-core:3.22.0'
dependency 'org.hamcrest:hamcrest:2.2'
dependency 'org.hsqldb:hsqldb:2.6.1'
dependency 'org.mariadb.jdbc:mariadb-java-client:2.7.4'
dependencySet(group: 'org.mockito', version: '4.2.0') {
dependency 'org.hsqldb:hsqldb:2.5.2'
dependency 'org.mariadb.jdbc:mariadb-java-client:2.7.5'
dependencySet(group: 'org.mockito', version: '4.4.0') {
entry 'mockito-core'
entry 'mockito-junit-jupiter'
}
dependencySet(group: 'org.mongodb', version: '4.6.0') {
dependencySet(group: 'org.mongodb', version: '4.5.1') {
entry 'mongodb-driver-core'
entry 'mongodb-driver-sync'
entry 'mongodb-driver-reactivestreams'
}
dependency 'org.postgresql:postgresql:42.3.1'
dependency 'org.postgresql:postgresql:42.3.4'
}
}

Binary file not shown.

View File

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

View File

@@ -18,6 +18,8 @@ include 'spring-session-data-redis'
include 'spring-session-docs'
include 'spring-session-hazelcast'
include 'spring-session-jdbc'
include 'hazelcast4'
project(':hazelcast4').projectDir = file('spring-session-hazelcast/hazelcast4')
file('spring-session-samples').eachDirMatch(~/spring-session-sample-.*/) { dir ->
include dir.name

View File

@@ -19,10 +19,10 @@ package org.springframework.session.config.annotation.web.http;
import java.util.ArrayList;
import java.util.List;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.ServletContext;
import jakarta.servlet.SessionCookieConfig;
import jakarta.servlet.http.HttpSessionListener;
import javax.annotation.PostConstruct;
import javax.servlet.ServletContext;
import javax.servlet.SessionCookieConfig;
import javax.servlet.http.HttpSessionListener;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

View File

@@ -16,9 +16,9 @@
package org.springframework.session.security.web.authentication;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2021 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.
@@ -19,10 +19,10 @@ package org.springframework.session.web.context;
import java.util.Arrays;
import java.util.EnumSet;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterRegistration.Dynamic;
import jakarta.servlet.ServletContext;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterRegistration.Dynamic;
import javax.servlet.ServletContext;
import org.springframework.context.ApplicationContext;
import org.springframework.core.Conventions;

View File

@@ -18,8 +18,8 @@ package org.springframework.session.web.http;
import java.util.List;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.session.web.http.CookieSerializer.CookieValue;
@@ -32,8 +32,8 @@ import org.springframework.session.web.http.CookieSerializer.CookieValue;
* When a session is created, the HTTP response will have a cookie with the specified
* cookie name and the value of the session id. The cookie will be marked as a session
* cookie, use the context path for the path of the cookie, marked as HTTPOnly, and if
* {@link jakarta.servlet.http.HttpServletRequest#isSecure()} returns true, the cookie
* will be marked as secure. For example:
* {@link javax.servlet.http.HttpServletRequest#isSecure()} returns true, the cookie will
* be marked as secure. For example:
*
* <pre>
* HTTP/1.1 200 OK

View File

@@ -18,9 +18,9 @@ package org.springframework.session.web.http;
import java.util.List;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Strategy for reading and writing a cookie value to the {@link HttpServletResponse}.

View File

@@ -28,9 +28,9 @@ import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -88,8 +88,7 @@ public class DefaultCookieSerializer implements CookieSerializer {
private String sameSite = "Lax";
/*
* @see
* org.springframework.session.web.http.CookieSerializer#readCookieValues(jakarta.
* @see org.springframework.session.web.http.CookieSerializer#readCookieValues(javax.
* servlet.http.HttpServletRequest)
*/
@Override

View File

@@ -19,8 +19,8 @@ package org.springframework.session.web.http;
import java.util.Collections;
import java.util.List;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* A {@link HttpSessionIdResolver} that uses a header to resolve the session id.

View File

@@ -22,11 +22,11 @@ import java.util.Enumeration;
import java.util.NoSuchElementException;
import java.util.Set;
import jakarta.servlet.ServletContext;
import jakarta.servlet.http.HttpSession;
import jakarta.servlet.http.HttpSessionBindingEvent;
import jakarta.servlet.http.HttpSessionBindingListener;
import jakarta.servlet.http.HttpSessionContext;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionBindingListener;
import javax.servlet.http.HttpSessionContext;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

View File

@@ -18,8 +18,8 @@ package org.springframework.session.web.http;
import java.util.List;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Contract for session id resolution strategies. Allows for session id resolution through

View File

@@ -20,14 +20,14 @@ import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.WriteListener;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponseWrapper;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
/**
* Base class for response wrappers which encapsulate the logic for handling an event when
* the {@link jakarta.servlet.http.HttpServletResponse} is committed.
* the {@link javax.servlet.http.HttpServletResponse} is committed.
*
* @author Rob Winch
* @since 1.0
@@ -84,16 +84,16 @@ abstract class OnCommittedResponseWrapper extends HttpServletResponseWrapper {
/**
* Invoke this method to disable invoking
* {@link OnCommittedResponseWrapper#onResponseCommitted()} when the
* {@link jakarta.servlet.http.HttpServletResponse} is committed. This can be useful
* in the event that Async Web Requests are made.
* {@link javax.servlet.http.HttpServletResponse} is committed. This can be useful in
* the event that Async Web Requests are made.
*/
private void disableOnResponseCommitted() {
this.disableOnCommitted = true;
}
/**
* Implement the logic for handling the
* {@link jakarta.servlet.http.HttpServletResponse} being committed.
* Implement the logic for handling the {@link javax.servlet.http.HttpServletResponse}
* being committed.
*/
protected abstract void onResponseCommitted();
@@ -474,9 +474,8 @@ abstract class OnCommittedResponseWrapper extends HttpServletResponseWrapper {
/**
* Ensures{@link OnCommittedResponseWrapper#onResponseCommitted()} is invoked before
* calling methods that commit the response. We delegate all methods to the original
* {@link jakarta.servlet.ServletOutputStream} to ensure that the behavior is as close
* to the original {@link jakarta.servlet.ServletOutputStream} as possible. See
* SEC-2039
* {@link javax.servlet.ServletOutputStream} to ensure that the behavior is as close
* to the original {@link javax.servlet.ServletOutputStream} as possible. See SEC-2039
*
* @author Rob Winch
*/

View File

@@ -18,15 +18,15 @@ package org.springframework.session.web.http;
import java.io.IOException;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Allows for easily ensuring that a request is only invoked once per request. This is a

View File

@@ -18,10 +18,10 @@ package org.springframework.session.web.http;
import java.util.List;
import jakarta.servlet.ServletContext;
import jakarta.servlet.http.HttpSession;
import jakarta.servlet.http.HttpSessionEvent;
import jakarta.servlet.http.HttpSessionListener;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import org.springframework.context.ApplicationListener;
import org.springframework.session.Session;
@@ -79,8 +79,9 @@ public class SessionEventHttpSessionListenerAdapter
}
/*
* @see org.springframework.web.context.ServletContextAware#setServletContext(jakarta.
* servlet.ServletContext)
* @see
* org.springframework.web.context.ServletContextAware#setServletContext(javax.servlet
* .ServletContext)
*/
@Override
public void setServletContext(ServletContext servletContext) {

View File

@@ -20,16 +20,16 @@ import java.io.IOException;
import java.time.Instant;
import java.util.List;
import jakarta.servlet.FilterChain;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import javax.servlet.FilterChain;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -39,29 +39,29 @@ import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
/**
* Switches the {@link jakarta.servlet.http.HttpSession} implementation to be backed by a
* Switches the {@link javax.servlet.http.HttpSession} implementation to be backed by a
* {@link org.springframework.session.Session}.
*
* The {@link SessionRepositoryFilter} wraps the
* {@link jakarta.servlet.http.HttpServletRequest} and overrides the methods to get an
* {@link jakarta.servlet.http.HttpSession} to be backed by a
* {@link javax.servlet.http.HttpServletRequest} and overrides the methods to get an
* {@link javax.servlet.http.HttpSession} to be backed by a
* {@link org.springframework.session.Session} returned by the
* {@link org.springframework.session.SessionRepository}.
*
* The {@link SessionRepositoryFilter} uses a {@link HttpSessionIdResolver} (default
* {@link CookieHttpSessionIdResolver}) to bridge logic between an
* {@link jakarta.servlet.http.HttpSession} and the
* {@link javax.servlet.http.HttpSession} and the
* {@link org.springframework.session.Session} abstraction. Specifically:
*
* <ul>
* <li>The session id is looked up using
* {@link HttpSessionIdResolver#resolveSessionIds(jakarta.servlet.http.HttpServletRequest)}
* {@link HttpSessionIdResolver#resolveSessionIds(javax.servlet.http.HttpServletRequest)}
* . The default is to look in a cookie named SESSION.</li>
* <li>The session id of newly created {@link org.springframework.session.Session} is sent
* to the client using
* {@link HttpSessionIdResolver#setSessionId(jakarta.servlet.http.HttpServletRequest, jakarta.servlet.http.HttpServletResponse, String)}
* {@link HttpSessionIdResolver#setSessionId(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, String)}
* <li>The client is notified that the session id is no longer valid with
* {@link HttpSessionIdResolver#expireSession(jakarta.servlet.http.HttpServletRequest, jakarta.servlet.http.HttpServletResponse)}
* {@link HttpSessionIdResolver#expireSession(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)}
* </li>
* </ul>
*
@@ -183,8 +183,8 @@ public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFi
}
/**
* A {@link jakarta.servlet.http.HttpServletRequest} that retrieves the
* {@link jakarta.servlet.http.HttpSession} using a
* A {@link javax.servlet.http.HttpServletRequest} that retrieves the
* {@link javax.servlet.http.HttpSession} using a
* {@link org.springframework.session.SessionRepository}.
*
* @author Rob Winch

View File

@@ -21,7 +21,7 @@ import java.util.EnumSet;
import java.util.Map;
import java.util.Set;
import jakarta.servlet.http.HttpSession;
import javax.servlet.http.HttpSession;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;

View File

@@ -19,10 +19,10 @@ package org.springframework.session.config.annotation.web.http;
import java.io.IOException;
import java.util.Collections;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

View File

@@ -18,7 +18,7 @@ package org.springframework.session.config.annotation.web.http;
import java.util.concurrent.ConcurrentHashMap;
import jakarta.servlet.ServletContext;
import javax.servlet.ServletContext;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

View File

@@ -16,9 +16,9 @@
package org.springframework.session.security.web.authentication;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.junit.jupiter.api.Test;

View File

@@ -19,7 +19,7 @@ package org.springframework.session.web.http;
import java.util.Base64;
import java.util.Collections;
import jakarta.servlet.http.Cookie;
import javax.servlet.http.Cookie;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

View File

@@ -23,7 +23,7 @@ import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Base64;
import jakarta.servlet.http.Cookie;
import javax.servlet.http.Cookie;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

View File

@@ -21,8 +21,8 @@ import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2021 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.
@@ -20,12 +20,12 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.DispatcherType;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2021 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.
@@ -19,9 +19,9 @@ package org.springframework.session.web.http;
import java.util.Arrays;
import java.util.Collections;
import jakarta.servlet.ServletContext;
import jakarta.servlet.http.HttpSessionEvent;
import jakarta.servlet.http.HttpSessionListener;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

View File

@@ -30,17 +30,17 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import jakarta.servlet.http.HttpSessionBindingEvent;
import jakarta.servlet.http.HttpSessionBindingListener;
import jakarta.servlet.http.HttpSessionContext;
import javax.servlet.FilterChain;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionBindingListener;
import javax.servlet.http.HttpSessionContext;
import org.assertj.core.data.Offset;
import org.junit.jupiter.api.BeforeEach;

View File

@@ -23,7 +23,7 @@ import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import jakarta.servlet.http.HttpSession;
import javax.servlet.http.HttpSession;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

View File

@@ -389,7 +389,7 @@ public abstract class AbstractMongoRepositoryITest extends AbstractITest {
protected static class BaseConfig {
private static final String DOCKER_IMAGE = "mongo:4.0.10";
private static final String DOCKER_IMAGE = "mongo:4.4.1";
@Bean(initMethod = "start", destroyMethod = "stop")
public MongoDBContainer mongoContainer() {

View File

@@ -175,7 +175,7 @@ public class MongoDbDeleteJacksonSessionVerificationTest {
@EnableMongoWebSession
static class Config {
private static final String DOCKER_IMAGE = "mongo:4.0.10";
private static final String DOCKER_IMAGE = "mongo:4.4.1";
@Bean(initMethod = "start", destroyMethod = "stop")
MongoDBContainer mongoContainer() {

View File

@@ -170,7 +170,7 @@ public class MongoDbLogoutVerificationTest {
@EnableMongoWebSession
static class Config {
private static final String DOCKER_IMAGE = "mongo:4.0.10";
private static final String DOCKER_IMAGE = "mongo:4.4.1";
@Bean(initMethod = "start", destroyMethod = "stop")
MongoDBContainer mongoContainer() {

View File

@@ -29,7 +29,7 @@ import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactor
*/
public abstract class AbstractRedisITests {
private static final String DOCKER_IMAGE = "redis:5.0.10";
private static final String DOCKER_IMAGE = "redis:5.0.14";
protected static class BaseConfig {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2022 the original author or authors.
* Copyright 2014-2021 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.
@@ -691,7 +691,7 @@ class RedisIndexedSessionRepositoryITests extends AbstractRedisITests {
}
@Configuration
@EnableRedisHttpSession(redisNamespace = "RedisIndexedSessionRepositoryITests", enableIndexingAndEvents = true)
@EnableRedisHttpSession(redisNamespace = "RedisIndexedSessionRepositoryITests")
static class Config extends BaseConfig {
@Bean

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2022 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.
@@ -26,10 +26,13 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.RedisSessionRepository.RedisSession;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
@@ -220,9 +223,17 @@ class RedisSessionRepositoryITests extends AbstractRedisITests {
}
@Configuration
@EnableRedisHttpSession
@EnableSpringHttpSession
static class Config extends BaseConfig {
@Bean
RedisSessionRepository sessionRepository(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.afterPropertiesSet();
return new RedisSessionRepository(redisTemplate);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2022 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.
@@ -113,7 +113,7 @@ class EnableRedisHttpSessionExpireSessionDestroyedTests<S extends Session> exten
}
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1, enableIndexingAndEvents = true)
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1)
static class Config extends BaseConfig {
@Bean

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2022 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.
@@ -101,7 +101,7 @@ class RedisListenerContainerTaskExecutorITests extends AbstractRedisITests {
}
@Configuration
@EnableRedisHttpSession(redisNamespace = "RedisListenerContainerTaskExecutorITests", enableIndexingAndEvents = true)
@EnableRedisHttpSession(redisNamespace = "RedisListenerContainerTaskExecutorITests")
static class Config extends BaseConfig {
@Bean

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2022 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.
@@ -33,7 +33,6 @@ 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.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.RedisSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
/**
@@ -55,8 +54,7 @@ import org.springframework.session.web.http.SessionRepositoryFilter;
* }
* </pre>
*
* More advanced configurations can extend {@link RedisHttpSessionConfiguration} or
* {@link RedisIndexedHttpSessionConfiguration} instead.
* More advanced configurations can extend {@link RedisHttpSessionConfiguration} instead.
*
* @author Rob Winch
* @author Vedran Pavic
@@ -66,7 +64,7 @@ import org.springframework.session.web.http.SessionRepositoryFilter;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(RedisHttpSessionConfigurationSelector.class)
@Import(RedisHttpSessionConfiguration.class)
@Configuration(proxyBeanMethods = false)
public @interface EnableRedisHttpSession {
@@ -115,6 +113,13 @@ public @interface EnableRedisHttpSession {
*/
FlushMode flushMode() default FlushMode.ON_SAVE;
/**
* The cron expression for expired session cleanup job. By default runs every minute.
* @return the session cleanup cron expression
* @since 2.0.0
*/
String cleanupCron() default RedisHttpSessionConfiguration.DEFAULT_CLEANUP_CRON;
/**
* Save mode for the session. The default is {@link SaveMode#ON_SET_ATTRIBUTE}, which
* only saves changes made to session.
@@ -123,13 +128,4 @@ public @interface EnableRedisHttpSession {
*/
SaveMode saveMode() default SaveMode.ON_SET_ATTRIBUTE;
/**
* Indicate whether the {@link SessionRepository} should publish session events and
* support fetching sessions by index. If true, a
* {@link RedisIndexedSessionRepository} will be used in place of
* {@link RedisSessionRepository}. This will result in slower performance.
* @return true if indexing and events should be enabled, false otherwise
*/
boolean enableIndexingAndEvents() default false;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2022 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.
@@ -16,35 +16,55 @@
package org.springframework.session.data.redis.config.annotation.web.http;
import java.time.Duration;
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;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportAware;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
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.RedisSessionRepository;
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;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.StringValueResolver;
@@ -63,39 +83,86 @@ import org.springframework.util.StringValueResolver;
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware {
static final String DEFAULT_CLEANUP_CRON = "0 * * * * *";
private Integer maxInactiveIntervalInSeconds = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
private String redisNamespace = RedisSessionRepository.DEFAULT_KEY_NAMESPACE;
private String redisNamespace = RedisIndexedSessionRepository.DEFAULT_NAMESPACE;
private FlushMode flushMode = FlushMode.ON_SAVE;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
private String cleanupCron = DEFAULT_CLEANUP_CRON;
private ConfigureRedisAction configureRedisAction = new ConfigureNotifyKeyspaceEventsAction();
private RedisConnectionFactory redisConnectionFactory;
private IndexResolver<Session> indexResolver;
private RedisSerializer<Object> defaultRedisSerializer;
private List<SessionRepositoryCustomizer<RedisSessionRepository>> sessionRepositoryCustomizers;
private ApplicationEventPublisher applicationEventPublisher;
private Executor redisTaskExecutor;
private Executor redisSubscriptionExecutor;
private List<SessionRepositoryCustomizer<RedisIndexedSessionRepository>> sessionRepositoryCustomizers;
private ClassLoader classLoader;
private StringValueResolver embeddedValueResolver;
@Bean
public RedisSessionRepository sessionRepository() {
RedisTemplate<String, Object> redisTemplate = createRedisTemplate();
RedisSessionRepository sessionRepository = new RedisSessionRepository(redisTemplate);
sessionRepository.setDefaultMaxInactiveInterval(Duration.ofSeconds(this.maxInactiveIntervalInSeconds));
public RedisIndexedSessionRepository sessionRepository() {
RedisTemplate<Object, Object> redisTemplate = createRedisTemplate();
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);
}
sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
if (StringUtils.hasText(this.redisNamespace)) {
sessionRepository.setRedisKeyNamespace(this.redisNamespace);
}
sessionRepository.setFlushMode(this.flushMode);
sessionRepository.setSaveMode(this.saveMode);
int database = resolveDatabase();
sessionRepository.setDatabase(database);
this.sessionRepositoryCustomizers
.forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository));
return sessionRepository;
}
@Bean
public RedisMessageListenerContainer springSessionRedisMessageListenerContainer(
RedisIndexedSessionRepository sessionRepository) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(this.redisConnectionFactory);
if (this.redisTaskExecutor != null) {
container.setTaskExecutor(this.redisTaskExecutor);
}
if (this.redisSubscriptionExecutor != null) {
container.setSubscriptionExecutor(this.redisSubscriptionExecutor);
}
container.addMessageListener(sessionRepository,
Arrays.asList(new ChannelTopic(sessionRepository.getSessionDeletedChannel()),
new ChannelTopic(sessionRepository.getSessionExpiredChannel())));
container.addMessageListener(sessionRepository,
Collections.singletonList(new PatternTopic(sessionRepository.getSessionCreatedChannelPrefix() + "*")));
return container;
}
@Bean
public InitializingBean enableRedisKeyspaceNotificationsInitializer() {
return new EnableRedisKeyspaceNotificationsInitializer(this.redisConnectionFactory, this.configureRedisAction);
}
public void setMaxInactiveIntervalInSeconds(int maxInactiveIntervalInSeconds) {
this.maxInactiveIntervalInSeconds = maxInactiveIntervalInSeconds;
}
@@ -119,6 +186,20 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
this.saveMode = saveMode;
}
public void setCleanupCron(String cleanupCron) {
this.cleanupCron = cleanupCron;
}
/**
* Sets the action to perform for configuring Redis.
* @param configureRedisAction the configureRedis to set. The default is
* {@link ConfigureNotifyKeyspaceEventsAction}.
*/
@Autowired(required = false)
public void setConfigureRedisAction(ConfigureRedisAction configureRedisAction) {
this.configureRedisAction = configureRedisAction;
}
@Autowired
public void setRedisConnectionFactory(
@SpringSessionRedisConnectionFactory ObjectProvider<RedisConnectionFactory> springSessionRedisConnectionFactory,
@@ -136,9 +217,31 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
this.defaultRedisSerializer = defaultRedisSerializer;
}
@Autowired
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
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) {
this.redisTaskExecutor = redisTaskExecutor;
}
@Autowired(required = false)
@Qualifier("springSessionRedisSubscriptionExecutor")
public void setRedisSubscriptionExecutor(Executor redisSubscriptionExecutor) {
this.redisSubscriptionExecutor = redisSubscriptionExecutor;
}
@Autowired(required = false)
public void setSessionRepositoryCustomizer(
ObjectProvider<SessionRepositoryCustomizer<RedisSessionRepository>> sessionRepositoryCustomizers) {
ObjectProvider<SessionRepositoryCustomizer<RedisIndexedSessionRepository>> sessionRepositoryCustomizers) {
this.sessionRepositoryCustomizers = sessionRepositoryCustomizers.orderedStream().collect(Collectors.toList());
}
@@ -170,10 +273,14 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
}
this.flushMode = flushMode;
this.saveMode = attributes.getEnum("saveMode");
String cleanupCron = attributes.getString("cleanupCron");
if (StringUtils.hasText(cleanupCron)) {
this.cleanupCron = cleanupCron;
}
}
private RedisTemplate<String, Object> createRedisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
private RedisTemplate<Object, Object> createRedisTemplate() {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
if (this.defaultRedisSerializer != null) {
@@ -185,4 +292,77 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
return redisTemplate;
}
private int resolveDatabase() {
if (ClassUtils.isPresent("io.lettuce.core.RedisClient", null)
&& this.redisConnectionFactory instanceof LettuceConnectionFactory) {
return ((LettuceConnectionFactory) this.redisConnectionFactory).getDatabase();
}
if (ClassUtils.isPresent("redis.clients.jedis.Jedis", null)
&& this.redisConnectionFactory instanceof JedisConnectionFactory) {
return ((JedisConnectionFactory) this.redisConnectionFactory).getDatabase();
}
return RedisIndexedSessionRepository.DEFAULT_DATABASE;
}
/**
* Ensures that Redis is configured to send keyspace notifications. This is important
* to ensure that expiration and deletion of sessions trigger SessionDestroyedEvents.
* Without the SessionDestroyedEvent resources may not get cleaned up properly. For
* example, the mapping of the Session to WebSocket connections may not get cleaned
* up.
*/
static class EnableRedisKeyspaceNotificationsInitializer implements InitializingBean {
private final RedisConnectionFactory connectionFactory;
private ConfigureRedisAction configure;
EnableRedisKeyspaceNotificationsInitializer(RedisConnectionFactory connectionFactory,
ConfigureRedisAction configure) {
this.connectionFactory = connectionFactory;
this.configure = configure;
}
@Override
public void afterPropertiesSet() {
if (this.configure == ConfigureRedisAction.NO_OP) {
return;
}
RedisConnection connection = this.connectionFactory.getConnection();
try {
this.configure.configure(connection);
}
finally {
try {
connection.close();
}
catch (Exception ex) {
LogFactory.getLog(getClass()).error("Error closing RedisConnection", ex);
}
}
}
}
/**
* 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

@@ -1,46 +0,0 @@
/*
* Copyright 2014-2022 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.config.annotation.web.http;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
/**
* Dynamically determines which session repository configuration to include using the
* {@link EnableRedisHttpSession} annotation.
*
* @author Eleftheria Stein
* @since 3.0
*/
final class RedisHttpSessionConfigurationSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importMetadata) {
if (!importMetadata.hasAnnotation(EnableRedisHttpSession.class.getName())) {
return new String[0];
}
EnableRedisHttpSession annotation = importMetadata.getAnnotations().get(EnableRedisHttpSession.class)
.synthesize();
if (annotation.enableIndexingAndEvents()) {
return new String[] { RedisIndexedHttpSessionConfiguration.class.getName() };
}
else {
return new String[] { RedisHttpSessionConfiguration.class.getName() };
}
}
}

View File

@@ -1,361 +0,0 @@
/*
* Copyright 2014-2022 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.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;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportAware;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
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.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;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.StringValueResolver;
/**
* Exposes the {@link SessionRepositoryFilter} as a bean named
* {@code springSessionRepositoryFilter}. In order to use this a single
* {@link RedisConnectionFactory} must be exposed as a Bean.
*
* @author Eleftheria Stein
* @since 3.0
*/
@Configuration(proxyBeanMethods = false)
public class RedisIndexedHttpSessionConfiguration extends SpringHttpSessionConfiguration
implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware {
static final String DEFAULT_CLEANUP_CRON = "0 * * * * *";
private Integer maxInactiveIntervalInSeconds = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
private String redisNamespace = RedisIndexedSessionRepository.DEFAULT_NAMESPACE;
private FlushMode flushMode = FlushMode.ON_SAVE;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
private String cleanupCron = DEFAULT_CLEANUP_CRON;
private ConfigureRedisAction configureRedisAction = new ConfigureNotifyKeyspaceEventsAction();
private RedisConnectionFactory redisConnectionFactory;
private IndexResolver<Session> indexResolver;
private RedisSerializer<Object> defaultRedisSerializer;
private ApplicationEventPublisher applicationEventPublisher;
private Executor redisTaskExecutor;
private Executor redisSubscriptionExecutor;
private List<SessionRepositoryCustomizer<RedisIndexedSessionRepository>> sessionRepositoryCustomizers;
private ClassLoader classLoader;
private StringValueResolver embeddedValueResolver;
@Bean
public RedisIndexedSessionRepository sessionRepository() {
RedisTemplate<Object, Object> redisTemplate = createRedisTemplate();
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);
}
sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
if (StringUtils.hasText(this.redisNamespace)) {
sessionRepository.setRedisKeyNamespace(this.redisNamespace);
}
sessionRepository.setFlushMode(this.flushMode);
sessionRepository.setSaveMode(this.saveMode);
int database = resolveDatabase();
sessionRepository.setDatabase(database);
this.sessionRepositoryCustomizers
.forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository));
return sessionRepository;
}
@Bean
public RedisMessageListenerContainer springSessionRedisMessageListenerContainer(
RedisIndexedSessionRepository sessionRepository) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(this.redisConnectionFactory);
if (this.redisTaskExecutor != null) {
container.setTaskExecutor(this.redisTaskExecutor);
}
if (this.redisSubscriptionExecutor != null) {
container.setSubscriptionExecutor(this.redisSubscriptionExecutor);
}
container.addMessageListener(sessionRepository,
Arrays.asList(new ChannelTopic(sessionRepository.getSessionDeletedChannel()),
new ChannelTopic(sessionRepository.getSessionExpiredChannel())));
container.addMessageListener(sessionRepository,
Collections.singletonList(new PatternTopic(sessionRepository.getSessionCreatedChannelPrefix() + "*")));
return container;
}
@Bean
public InitializingBean enableRedisKeyspaceNotificationsInitializer() {
return new EnableRedisKeyspaceNotificationsInitializer(this.redisConnectionFactory, this.configureRedisAction);
}
public void setMaxInactiveIntervalInSeconds(int maxInactiveIntervalInSeconds) {
this.maxInactiveIntervalInSeconds = maxInactiveIntervalInSeconds;
}
public void setRedisNamespace(String namespace) {
this.redisNamespace = namespace;
}
@Deprecated
public void setRedisFlushMode(RedisFlushMode redisFlushMode) {
Assert.notNull(redisFlushMode, "redisFlushMode cannot be null");
setFlushMode(redisFlushMode.getFlushMode());
}
public void setFlushMode(FlushMode flushMode) {
Assert.notNull(flushMode, "flushMode cannot be null");
this.flushMode = flushMode;
}
public void setSaveMode(SaveMode saveMode) {
this.saveMode = saveMode;
}
public void setCleanupCron(String cleanupCron) {
this.cleanupCron = cleanupCron;
}
/**
* Sets the action to perform for configuring Redis.
* @param configureRedisAction the configureRedis to set. The default is
* {@link ConfigureNotifyKeyspaceEventsAction}.
*/
@Autowired(required = false)
public void setConfigureRedisAction(ConfigureRedisAction configureRedisAction) {
this.configureRedisAction = configureRedisAction;
}
@Autowired
public void setRedisConnectionFactory(
@SpringSessionRedisConnectionFactory ObjectProvider<RedisConnectionFactory> springSessionRedisConnectionFactory,
ObjectProvider<RedisConnectionFactory> redisConnectionFactory) {
RedisConnectionFactory redisConnectionFactoryToUse = springSessionRedisConnectionFactory.getIfAvailable();
if (redisConnectionFactoryToUse == null) {
redisConnectionFactoryToUse = redisConnectionFactory.getObject();
}
this.redisConnectionFactory = redisConnectionFactoryToUse;
}
@Autowired(required = false)
@Qualifier("springSessionDefaultRedisSerializer")
public void setDefaultRedisSerializer(RedisSerializer<Object> defaultRedisSerializer) {
this.defaultRedisSerializer = defaultRedisSerializer;
}
@Autowired
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
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) {
this.redisTaskExecutor = redisTaskExecutor;
}
@Autowired(required = false)
@Qualifier("springSessionRedisSubscriptionExecutor")
public void setRedisSubscriptionExecutor(Executor redisSubscriptionExecutor) {
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;
}
@Override
public void setEmbeddedValueResolver(StringValueResolver resolver) {
this.embeddedValueResolver = resolver;
}
@Override
@SuppressWarnings("deprecation")
public void setImportMetadata(AnnotationMetadata importMetadata) {
Map<String, Object> attributeMap = importMetadata
.getAnnotationAttributes(EnableRedisHttpSession.class.getName());
AnnotationAttributes attributes = AnnotationAttributes.fromMap(attributeMap);
this.maxInactiveIntervalInSeconds = attributes.getNumber("maxInactiveIntervalInSeconds");
String redisNamespaceValue = attributes.getString("redisNamespace");
if (StringUtils.hasText(redisNamespaceValue)) {
this.redisNamespace = this.embeddedValueResolver.resolveStringValue(redisNamespaceValue);
}
FlushMode flushMode = attributes.getEnum("flushMode");
RedisFlushMode redisFlushMode = attributes.getEnum("redisFlushMode");
if (flushMode == FlushMode.ON_SAVE && redisFlushMode != RedisFlushMode.ON_SAVE) {
flushMode = redisFlushMode.getFlushMode();
}
this.flushMode = flushMode;
this.saveMode = attributes.getEnum("saveMode");
}
private RedisTemplate<Object, Object> createRedisTemplate() {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
if (this.defaultRedisSerializer != null) {
redisTemplate.setDefaultSerializer(this.defaultRedisSerializer);
}
redisTemplate.setConnectionFactory(this.redisConnectionFactory);
redisTemplate.setBeanClassLoader(this.classLoader);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
private int resolveDatabase() {
if (ClassUtils.isPresent("io.lettuce.core.RedisClient", null)
&& this.redisConnectionFactory instanceof LettuceConnectionFactory) {
return ((LettuceConnectionFactory) this.redisConnectionFactory).getDatabase();
}
if (ClassUtils.isPresent("redis.clients.jedis.Jedis", null)
&& this.redisConnectionFactory instanceof JedisConnectionFactory) {
return ((JedisConnectionFactory) this.redisConnectionFactory).getDatabase();
}
return RedisIndexedSessionRepository.DEFAULT_DATABASE;
}
/**
* Ensures that Redis is configured to send keyspace notifications. This is important
* to ensure that expiration and deletion of sessions trigger SessionDestroyedEvents.
* Without the SessionDestroyedEvent resources may not get cleaned up properly. For
* example, the mapping of the Session to WebSocket connections may not get cleaned
* up.
*/
static class EnableRedisKeyspaceNotificationsInitializer implements InitializingBean {
private final RedisConnectionFactory connectionFactory;
private ConfigureRedisAction configure;
EnableRedisKeyspaceNotificationsInitializer(RedisConnectionFactory connectionFactory,
ConfigureRedisAction configure) {
this.connectionFactory = connectionFactory;
this.configure = configure;
}
@Override
public void afterPropertiesSet() {
if (this.configure == ConfigureRedisAction.NO_OP) {
return;
}
RedisConnection connection = this.connectionFactory.getConnection();
try {
this.configure.configure(connection);
}
finally {
try {
connection.close();
}
catch (Exception ex) {
LogFactory.getLog(getClass()).error("Error closing RedisConnection", ex);
}
}
}
}
/**
* 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,
RedisIndexedHttpSessionConfiguration.this.cleanupCron);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2022 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.
@@ -49,14 +49,14 @@ class EnableRedisKeyspaceNotificationsInitializerTests {
@Captor
ArgumentCaptor<String> options;
private RedisIndexedHttpSessionConfiguration.EnableRedisKeyspaceNotificationsInitializer initializer;
private RedisHttpSessionConfiguration.EnableRedisKeyspaceNotificationsInitializer initializer;
@BeforeEach
void setup() {
MockitoAnnotations.initMocks(this);
given(this.connectionFactory.getConnection()).willReturn(this.connection);
this.initializer = new RedisIndexedHttpSessionConfiguration.EnableRedisKeyspaceNotificationsInitializer(
this.initializer = new RedisHttpSessionConfiguration.EnableRedisKeyspaceNotificationsInitializer(
this.connectionFactory, new ConfigureNotifyKeyspaceEventsAction());
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2022 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.
@@ -24,7 +24,7 @@ import org.mockito.MockitoAnnotations;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.session.data.redis.config.ConfigureRedisAction;
import org.springframework.session.data.redis.config.annotation.web.http.RedisIndexedHttpSessionConfiguration.EnableRedisKeyspaceNotificationsInitializer;
import org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration.EnableRedisKeyspaceNotificationsInitializer;
import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown;
import static org.mockito.BDDMockito.given;
@@ -33,7 +33,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
class RedisIndexedHttpSessionConfigurationMockTests {
class RedisHttpSessionConfigurationMockTests {
@Mock
RedisConnectionFactory factory;

View File

@@ -48,7 +48,7 @@ import static org.mockito.Mockito.verify;
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
class RedisIndexedHttpSessionConfigurationOverrideSessionTaskExecutor {
class RedisHttpSessionConfigurationOverrideSessionTaskExecutor {
@Autowired
RedisMessageListenerContainer redisMessageListenerContainer;
@@ -61,7 +61,7 @@ class RedisIndexedHttpSessionConfigurationOverrideSessionTaskExecutor {
verify(this.springSessionRedisTaskExecutor, times(1)).execute(any(Runnable.class));
}
@EnableRedisHttpSession(enableIndexingAndEvents = true)
@EnableRedisHttpSession
@Configuration
static class Config {

View File

@@ -50,7 +50,7 @@ import static org.mockito.Mockito.verify;
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
class RedisIndexedHttpSessionConfigurationOverrideSessionTaskExecutors {
class RedisHttpSessionConfigurationOverrideSessionTaskExecutors {
@Autowired
RedisMessageListenerContainer redisMessageListenerContainer;
@@ -67,7 +67,7 @@ class RedisIndexedHttpSessionConfigurationOverrideSessionTaskExecutors {
verify(this.springSessionRedisTaskExecutor, never()).execute(any(Runnable.class));
}
@EnableRedisHttpSession(enableIndexingAndEvents = true)
@EnableRedisHttpSession
@Configuration
static class Config {

View File

@@ -16,7 +16,7 @@
package org.springframework.session.data.redis.config.annotation.web.http;
import java.time.Duration;
import java.util.Map;
import java.util.Properties;
import org.junit.jupiter.api.AfterEach;
@@ -34,12 +34,15 @@ import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.SubscriptionListener;
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.RedisSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory;
import org.springframework.test.util.ReflectionTestUtils;
@@ -59,7 +62,9 @@ import static org.mockito.Mockito.mock;
*/
class RedisHttpSessionConfigurationTests {
private static final Duration MAX_INACTIVE_INTERVAL_DURATION = Duration.ofSeconds(600);
private static final int MAX_INACTIVE_INTERVAL_IN_SECONDS = 600;
private static final String CLEANUP_CRON_EXPRESSION = "0 0 * * * *";
private AnnotationConfigApplicationContext context;
@@ -95,7 +100,7 @@ class RedisHttpSessionConfigurationTests {
@Test
void customFlushImmediately() {
registerAndRefresh(RedisConfig.class, CustomFlushImmediatelyConfiguration.class);
RedisSessionRepository sessionRepository = this.context.getBean(RedisSessionRepository.class);
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
assertThat(sessionRepository).isNotNull();
assertThat(ReflectionTestUtils.getField(sessionRepository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
}
@@ -103,7 +108,7 @@ class RedisHttpSessionConfigurationTests {
@Test
void customFlushImmediatelyLegacy() {
registerAndRefresh(RedisConfig.class, CustomFlushImmediatelyLegacyConfiguration.class);
RedisSessionRepository sessionRepository = this.context.getBean(RedisSessionRepository.class);
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
assertThat(sessionRepository).isNotNull();
assertThat(ReflectionTestUtils.getField(sessionRepository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
}
@@ -111,7 +116,7 @@ class RedisHttpSessionConfigurationTests {
@Test
void setCustomFlushImmediately() {
registerAndRefresh(RedisConfig.class, CustomFlushImmediatelySetConfiguration.class);
RedisSessionRepository sessionRepository = this.context.getBean(RedisSessionRepository.class);
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
assertThat(sessionRepository).isNotNull();
assertThat(ReflectionTestUtils.getField(sessionRepository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
}
@@ -119,22 +124,40 @@ class RedisHttpSessionConfigurationTests {
@Test
void setCustomFlushImmediatelyLegacy() {
registerAndRefresh(RedisConfig.class, CustomFlushImmediatelySetLegacyConfiguration.class);
RedisSessionRepository sessionRepository = this.context.getBean(RedisSessionRepository.class);
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
assertThat(sessionRepository).isNotNull();
assertThat(ReflectionTestUtils.getField(sessionRepository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
}
@Test
void customCleanupCronAnnotation() {
registerAndRefresh(RedisConfig.class, CustomCleanupCronExpressionAnnotationConfiguration.class);
RedisHttpSessionConfiguration configuration = this.context.getBean(RedisHttpSessionConfiguration.class);
assertThat(configuration).isNotNull();
assertThat(ReflectionTestUtils.getField(configuration, "cleanupCron")).isEqualTo(CLEANUP_CRON_EXPRESSION);
}
@Test
void customCleanupCronSetter() {
registerAndRefresh(RedisConfig.class, CustomCleanupCronExpressionSetterConfiguration.class);
RedisHttpSessionConfiguration configuration = this.context.getBean(RedisHttpSessionConfiguration.class);
assertThat(configuration).isNotNull();
assertThat(ReflectionTestUtils.getField(configuration, "cleanupCron")).isEqualTo(CLEANUP_CRON_EXPRESSION);
}
@Test
void customSaveModeAnnotation() {
registerAndRefresh(RedisConfig.class, CustomSaveModeExpressionAnnotationConfiguration.class);
assertThat(this.context.getBean(RedisSessionRepository.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(RedisSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
assertThat(this.context.getBean(RedisIndexedSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
SaveMode.ALWAYS);
}
@@ -142,7 +165,7 @@ class RedisHttpSessionConfigurationTests {
void qualifiedConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, QualifiedConnectionFactoryRedisConfig.class);
RedisSessionRepository repository = this.context.getBean(RedisSessionRepository.class);
RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class);
RedisConnectionFactory redisConnectionFactory = this.context.getBean("qualifiedRedisConnectionFactory",
RedisConnectionFactory.class);
assertThat(repository).isNotNull();
@@ -158,7 +181,7 @@ class RedisHttpSessionConfigurationTests {
void primaryConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, PrimaryConnectionFactoryRedisConfig.class);
RedisSessionRepository repository = this.context.getBean(RedisSessionRepository.class);
RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class);
RedisConnectionFactory redisConnectionFactory = this.context.getBean("primaryRedisConnectionFactory",
RedisConnectionFactory.class);
assertThat(repository).isNotNull();
@@ -174,7 +197,7 @@ class RedisHttpSessionConfigurationTests {
void qualifiedAndPrimaryConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, QualifiedAndPrimaryConnectionFactoryRedisConfig.class);
RedisSessionRepository repository = this.context.getBean(RedisSessionRepository.class);
RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class);
RedisConnectionFactory redisConnectionFactory = this.context.getBean("qualifiedRedisConnectionFactory",
RedisConnectionFactory.class);
assertThat(repository).isNotNull();
@@ -190,7 +213,7 @@ class RedisHttpSessionConfigurationTests {
void namedConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, NamedConnectionFactoryRedisConfig.class);
RedisSessionRepository repository = this.context.getBean(RedisSessionRepository.class);
RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class);
RedisConnectionFactory redisConnectionFactory = this.context.getBean("redisConnectionFactory",
RedisConnectionFactory.class);
assertThat(repository).isNotNull();
@@ -209,12 +232,32 @@ 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);
Map<String, RedisMessageListenerContainer> beans = this.context
.getBeansOfType(RedisMessageListenerContainer.class);
assertThat(beans).hasSize(2);
assertThat(beans).containsKeys("springSessionRedisMessageListenerContainer", "redisMessageListenerContainer");
}
@Test
void sessionRepositoryCustomizer() {
registerAndRefresh(RedisConfig.class, SessionRepositoryCustomizerConfiguration.class);
RedisSessionRepository sessionRepository = this.context.getBean(RedisSessionRepository.class);
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
assertThat(sessionRepository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval",
MAX_INACTIVE_INTERVAL_DURATION);
MAX_INACTIVE_INTERVAL_IN_SECONDS);
}
private void registerAndRefresh(Class<?>... annotatedClasses) {
@@ -295,6 +338,20 @@ class RedisHttpSessionConfigurationTests {
}
@EnableRedisHttpSession(cleanupCron = CLEANUP_CRON_EXPRESSION)
static class CustomCleanupCronExpressionAnnotationConfiguration {
}
@Configuration
static class CustomCleanupCronExpressionSetterConfiguration extends RedisHttpSessionConfiguration {
CustomCleanupCronExpressionSetterConfiguration() {
setCleanupCron(CLEANUP_CRON_EXPRESSION);
}
}
@EnableRedisHttpSession(saveMode = SaveMode.ALWAYS)
static class CustomSaveModeExpressionAnnotationConfiguration {
@@ -385,20 +442,43 @@ class RedisHttpSessionConfigurationTests {
}
@Configuration
@EnableRedisHttpSession
static class CustomIndexResolverConfiguration {
@Bean
@SuppressWarnings("unchecked")
IndexResolver<Session> indexResolver() {
return mock(IndexResolver.class);
}
}
@Configuration
@EnableRedisHttpSession
static class CustomRedisMessageListenerContainerConfig {
@Bean
RedisMessageListenerContainer redisMessageListenerContainer() {
return mock(RedisMessageListenerContainer.class);
}
}
@EnableRedisHttpSession
static class SessionRepositoryCustomizerConfiguration {
@Bean
@Order(0)
SessionRepositoryCustomizer<RedisSessionRepository> sessionRepositoryCustomizerOne() {
return (sessionRepository) -> sessionRepository.setDefaultMaxInactiveInterval(Duration.ZERO);
SessionRepositoryCustomizer<RedisIndexedSessionRepository> sessionRepositoryCustomizerOne() {
return (sessionRepository) -> sessionRepository.setDefaultMaxInactiveInterval(0);
}
@Bean
@Order(1)
SessionRepositoryCustomizer<RedisSessionRepository> sessionRepositoryCustomizerTwo() {
SessionRepositoryCustomizer<RedisIndexedSessionRepository> sessionRepositoryCustomizerTwo() {
return (sessionRepository) -> sessionRepository
.setDefaultMaxInactiveInterval(MAX_INACTIVE_INTERVAL_DURATION);
.setDefaultMaxInactiveInterval(MAX_INACTIVE_INTERVAL_IN_SECONDS);
}
}

View File

@@ -1,471 +0,0 @@
/*
* Copyright 2014-2022 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.config.annotation.web.http;
import java.util.Map;
import java.util.Properties;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.BeanCreationException;
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.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.connection.SubscriptionListener;
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.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link RedisIndexedHttpSessionConfiguration}.
*/
class RedisIndexedHttpSessionConfigurationTests {
private static final int MAX_INACTIVE_INTERVAL_IN_SECONDS = 600;
private static final String CLEANUP_CRON_EXPRESSION = "0 0 * * * *";
private AnnotationConfigApplicationContext context;
@BeforeEach
void before() {
this.context = new AnnotationConfigApplicationContext();
}
@AfterEach
void after() {
if (this.context != null) {
this.context.close();
}
}
@Test
void resolveValue() {
registerAndRefresh(RedisConfig.class, CustomRedisHttpSessionConfiguration.class);
RedisIndexedHttpSessionConfiguration configuration = this.context
.getBean(RedisIndexedHttpSessionConfiguration.class);
assertThat(ReflectionTestUtils.getField(configuration, "redisNamespace")).isEqualTo("myRedisNamespace");
}
@Test
void resolveValueByPlaceholder() {
this.context
.setEnvironment(new MockEnvironment().withProperty("session.redis.namespace", "customRedisNamespace"));
registerAndRefresh(RedisConfig.class, PropertySourceConfiguration.class,
CustomRedisHttpSessionConfiguration2.class);
RedisIndexedHttpSessionConfiguration configuration = this.context
.getBean(RedisIndexedHttpSessionConfiguration.class);
assertThat(ReflectionTestUtils.getField(configuration, "redisNamespace")).isEqualTo("customRedisNamespace");
}
@Test
void customFlushImmediately() {
registerAndRefresh(RedisConfig.class, CustomFlushImmediatelyConfiguration.class);
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
assertThat(sessionRepository).isNotNull();
assertThat(ReflectionTestUtils.getField(sessionRepository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
}
@Test
void customFlushImmediatelyLegacy() {
registerAndRefresh(RedisConfig.class, CustomFlushImmediatelyLegacyConfiguration.class);
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
assertThat(sessionRepository).isNotNull();
assertThat(ReflectionTestUtils.getField(sessionRepository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
}
@Test
void setCustomFlushImmediately() {
registerAndRefresh(RedisConfig.class, CustomFlushImmediatelySetConfiguration.class);
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
assertThat(sessionRepository).isNotNull();
assertThat(ReflectionTestUtils.getField(sessionRepository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
}
@Test
void setCustomFlushImmediatelyLegacy() {
registerAndRefresh(RedisConfig.class, CustomFlushImmediatelySetLegacyConfiguration.class);
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
assertThat(sessionRepository).isNotNull();
assertThat(ReflectionTestUtils.getField(sessionRepository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
}
@Test
void customCleanupCronSetter() {
registerAndRefresh(RedisConfig.class, CustomCleanupCronExpressionSetterConfiguration.class);
RedisIndexedHttpSessionConfiguration configuration = this.context
.getBean(RedisIndexedHttpSessionConfiguration.class);
assertThat(configuration).isNotNull();
assertThat(ReflectionTestUtils.getField(configuration, "cleanupCron")).isEqualTo(CLEANUP_CRON_EXPRESSION);
}
@Test
void customSaveModeAnnotation() {
registerAndRefresh(RedisConfig.class, CustomSaveModeExpressionAnnotationConfiguration.class);
assertThat(this.context.getBean(RedisIndexedSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
SaveMode.ALWAYS);
}
@Test
void customSaveModeSetter() {
registerAndRefresh(RedisConfig.class, CustomSaveModeExpressionSetterConfiguration.class);
assertThat(this.context.getBean(RedisIndexedSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
SaveMode.ALWAYS);
}
@Test
void qualifiedConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, QualifiedConnectionFactoryRedisConfig.class);
RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class);
RedisConnectionFactory redisConnectionFactory = this.context.getBean("qualifiedRedisConnectionFactory",
RedisConnectionFactory.class);
assertThat(repository).isNotNull();
assertThat(redisConnectionFactory).isNotNull();
RedisOperations redisOperations = (RedisOperations) ReflectionTestUtils.getField(repository,
"sessionRedisOperations");
assertThat(redisOperations).isNotNull();
assertThat(ReflectionTestUtils.getField(redisOperations, "connectionFactory"))
.isEqualTo(redisConnectionFactory);
}
@Test
void primaryConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, PrimaryConnectionFactoryRedisConfig.class);
RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class);
RedisConnectionFactory redisConnectionFactory = this.context.getBean("primaryRedisConnectionFactory",
RedisConnectionFactory.class);
assertThat(repository).isNotNull();
assertThat(redisConnectionFactory).isNotNull();
RedisOperations redisOperations = (RedisOperations) ReflectionTestUtils.getField(repository,
"sessionRedisOperations");
assertThat(redisOperations).isNotNull();
assertThat(ReflectionTestUtils.getField(redisOperations, "connectionFactory"))
.isEqualTo(redisConnectionFactory);
}
@Test
void qualifiedAndPrimaryConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, QualifiedAndPrimaryConnectionFactoryRedisConfig.class);
RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class);
RedisConnectionFactory redisConnectionFactory = this.context.getBean("qualifiedRedisConnectionFactory",
RedisConnectionFactory.class);
assertThat(repository).isNotNull();
assertThat(redisConnectionFactory).isNotNull();
RedisOperations redisOperations = (RedisOperations) ReflectionTestUtils.getField(repository,
"sessionRedisOperations");
assertThat(redisOperations).isNotNull();
assertThat(ReflectionTestUtils.getField(redisOperations, "connectionFactory"))
.isEqualTo(redisConnectionFactory);
}
@Test
void namedConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, NamedConnectionFactoryRedisConfig.class);
RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class);
RedisConnectionFactory redisConnectionFactory = this.context.getBean("redisConnectionFactory",
RedisConnectionFactory.class);
assertThat(repository).isNotNull();
assertThat(redisConnectionFactory).isNotNull();
RedisOperations redisOperations = (RedisOperations) ReflectionTestUtils.getField(repository,
"sessionRedisOperations");
assertThat(redisOperations).isNotNull();
assertThat(ReflectionTestUtils.getField(redisOperations, "connectionFactory"))
.isEqualTo(redisConnectionFactory);
}
@Test
void multipleConnectionFactoryRedisConfig() {
assertThatExceptionOfType(BeanCreationException.class)
.isThrownBy(() -> registerAndRefresh(RedisConfig.class, MultipleConnectionFactoryRedisConfig.class))
.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);
Map<String, RedisMessageListenerContainer> beans = this.context
.getBeansOfType(RedisMessageListenerContainer.class);
assertThat(beans).hasSize(2);
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();
}
private static RedisConnectionFactory mockRedisConnectionFactory() {
RedisConnectionFactory connectionFactoryMock = mock(RedisConnectionFactory.class);
RedisConnection connectionMock = mock(RedisConnection.class);
given(connectionFactoryMock.getConnection()).willReturn(connectionMock);
Properties keyspaceEventsConfig = new Properties();
keyspaceEventsConfig.put("notify-keyspace-events", "KEA");
given(connectionMock.getConfig("notify-keyspace-events")).willReturn(keyspaceEventsConfig);
willAnswer((it) -> {
SubscriptionListener listener = it.getArgument(0);
listener.onPatternSubscribed(it.getArgument(1), 0);
listener.onChannelSubscribed("__keyevent@0__:del".getBytes(), 0);
listener.onChannelSubscribed("__keyevent@0__:expired".getBytes(), 0);
return null;
}).given(connectionMock).pSubscribe(any(), any());
return connectionFactoryMock;
}
@Configuration
static class PropertySourceConfiguration {
@Bean
PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
@Configuration
static class RedisConfig {
@Bean
RedisConnectionFactory defaultRedisConnectionFactory() {
return mockRedisConnectionFactory();
}
}
@Configuration
static class CustomFlushImmediatelySetConfiguration extends RedisIndexedHttpSessionConfiguration {
CustomFlushImmediatelySetConfiguration() {
setFlushMode(FlushMode.IMMEDIATE);
}
}
@Configuration
@SuppressWarnings("deprecation")
static class CustomFlushImmediatelySetLegacyConfiguration extends RedisIndexedHttpSessionConfiguration {
CustomFlushImmediatelySetLegacyConfiguration() {
setRedisFlushMode(RedisFlushMode.IMMEDIATE);
}
}
@Configuration
@EnableRedisHttpSession(flushMode = FlushMode.IMMEDIATE, enableIndexingAndEvents = true)
static class CustomFlushImmediatelyConfiguration {
}
@Configuration
@EnableRedisHttpSession(redisFlushMode = RedisFlushMode.IMMEDIATE, enableIndexingAndEvents = true)
@SuppressWarnings("deprecation")
static class CustomFlushImmediatelyLegacyConfiguration {
}
@Configuration
static class CustomCleanupCronExpressionSetterConfiguration extends RedisIndexedHttpSessionConfiguration {
CustomCleanupCronExpressionSetterConfiguration() {
setCleanupCron(CLEANUP_CRON_EXPRESSION);
}
}
@EnableRedisHttpSession(saveMode = SaveMode.ALWAYS, enableIndexingAndEvents = true)
static class CustomSaveModeExpressionAnnotationConfiguration {
}
@Configuration
static class CustomSaveModeExpressionSetterConfiguration extends RedisIndexedHttpSessionConfiguration {
CustomSaveModeExpressionSetterConfiguration() {
setSaveMode(SaveMode.ALWAYS);
}
}
@Configuration
@EnableRedisHttpSession(enableIndexingAndEvents = true)
static class QualifiedConnectionFactoryRedisConfig {
@Bean
@SpringSessionRedisConnectionFactory
RedisConnectionFactory qualifiedRedisConnectionFactory() {
return mockRedisConnectionFactory();
}
}
@Configuration
@EnableRedisHttpSession(enableIndexingAndEvents = true)
static class PrimaryConnectionFactoryRedisConfig {
@Bean
@Primary
RedisConnectionFactory primaryRedisConnectionFactory() {
return mockRedisConnectionFactory();
}
}
@Configuration
@EnableRedisHttpSession(enableIndexingAndEvents = true)
static class QualifiedAndPrimaryConnectionFactoryRedisConfig {
@Bean
@SpringSessionRedisConnectionFactory
RedisConnectionFactory qualifiedRedisConnectionFactory() {
return mockRedisConnectionFactory();
}
@Bean
@Primary
RedisConnectionFactory primaryRedisConnectionFactory() {
return mockRedisConnectionFactory();
}
}
@Configuration
@EnableRedisHttpSession(enableIndexingAndEvents = true)
static class NamedConnectionFactoryRedisConfig {
@Bean
RedisConnectionFactory redisConnectionFactory() {
return mockRedisConnectionFactory();
}
}
@Configuration
@EnableRedisHttpSession(enableIndexingAndEvents = true)
static class MultipleConnectionFactoryRedisConfig {
@Bean
RedisConnectionFactory secondaryRedisConnectionFactory() {
return mockRedisConnectionFactory();
}
}
@Configuration
@EnableRedisHttpSession(redisNamespace = "myRedisNamespace", enableIndexingAndEvents = true)
static class CustomRedisHttpSessionConfiguration {
}
@Configuration
@EnableRedisHttpSession(redisNamespace = "${session.redis.namespace}", enableIndexingAndEvents = true)
static class CustomRedisHttpSessionConfiguration2 {
}
@Configuration
@EnableRedisHttpSession(enableIndexingAndEvents = true)
static class CustomIndexResolverConfiguration {
@Bean
@SuppressWarnings("unchecked")
IndexResolver<Session> indexResolver() {
return mock(IndexResolver.class);
}
}
@Configuration
@EnableRedisHttpSession(enableIndexingAndEvents = true)
static class CustomRedisMessageListenerContainerConfig {
@Bean
RedisMessageListenerContainer redisMessageListenerContainer() {
return mock(RedisMessageListenerContainer.class);
}
}
@EnableRedisHttpSession(enableIndexingAndEvents = true)
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

@@ -9,7 +9,8 @@ content:
- url: https://github.com/spring-io/spring-generated-docs
branches: [spring-projects/spring-session/*]
- url: https://github.com/spring-projects/spring-session
branches: [main]
branches: [main,3.0.x,2.6.x]
tags: [ '3.0.*', '2.7.*', '2.6.*','!2.6.0-M*','!2.6.0-RC*','!2.7.0-M1','!3.0.0-M1']
start_path: spring-session-docs
urls:
latest_version_segment_strategy: redirect:to

View File

@@ -1,3 +1,3 @@
name: ROOT
version: '3.0.0-M3'
prerelease: 'true'
version: '2.7.0'
prerelease: '-SNAPSHOT'

View File

@@ -91,6 +91,7 @@ function createSymbolicVersionAlias (component, version, symbolicVersionSegment,
function computeOut (src, family, version, htmlUrlExtensionStyle) {
let { component, module: module_, basename, extname, relative, stem } = src
if (component === 'ROOT') component = ''
if (module_ === 'ROOT') module_ = ''
let indexifyPathSegment = ''
let familyPathSegment = ''
@@ -120,8 +121,11 @@ function computePub (src, out, family, version, htmlUrlExtensionStyle) {
const pub = {}
let url
if (family === 'nav') {
const urlSegments = version ? [src.component, version] : [src.component]
if (src.module && src.module !== 'ROOT') urlSegments.push(src.module)
const component = src.component || 'ROOT'
const urlSegments = component === 'ROOT' ? [] : [component]
if (version) urlSegments.push(version)
const module_ = src.module || 'ROOT'
if (module_ !== 'ROOT') urlSegments.push(module_)
// an artificial URL used for resolving page references in navigation model
url = '/' + urlSegments.join('/') + '/'
pub.moduleRootPath = '.'

View File

@@ -19,6 +19,9 @@ package docs;
import java.time.Duration;
import java.util.concurrent.ConcurrentHashMap;
import com.hazelcast.config.Config;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import org.junit.jupiter.api.Test;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
@@ -35,7 +38,7 @@ import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.RedisSessionRepository;
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.transaction.support.TransactionTemplate;
@@ -114,18 +117,6 @@ class IndexDocTests {
}
// end::expire-repository-demo[]
@Test
@SuppressWarnings("unused")
void newRedisSessionRepository() {
// tag::new-redissessionrepository[]
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// ... configure redisTemplate ...
SessionRepository<? extends Session> repository = new RedisSessionRepository(redisTemplate);
// end::new-redissessionrepository[]
}
@Test
@SuppressWarnings("unused")
void newRedisIndexedSessionRepository() {
@@ -180,21 +171,20 @@ class IndexDocTests {
// end::new-jdbcindexedsessionrepository[]
}
// @Test
// @SuppressWarnings("unused")
// void newHazelcastIndexedSessionRepository() {
// // tag::new-hazelcastindexedsessionrepository[]
//
// Config config = new Config();
//
// // ... configure Hazelcast ...
//
// HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(config);
//
// HazelcastIndexedSessionRepository repository = new
// HazelcastIndexedSessionRepository(hazelcastInstance);
// // end::new-hazelcastindexedsessionrepository[]
// }
@Test
@SuppressWarnings("unused")
void newHazelcastIndexedSessionRepository() {
// tag::new-hazelcastindexedsessionrepository[]
Config config = new Config();
// ... configure Hazelcast ...
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(config);
HazelcastIndexedSessionRepository repository = new HazelcastIndexedSessionRepository(hazelcastInstance);
// end::new-hazelcastindexedsessionrepository[]
}
@Test
void runSpringHttpSessionConfig() {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2021 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.
@@ -16,44 +16,40 @@
package docs.http;
// import com.hazelcast.config.AttributeConfig;
// import com.hazelcast.config.Config;
// import com.hazelcast.config.IndexConfig;
// import com.hazelcast.config.IndexType;
// import com.hazelcast.config.SerializerConfig;
// import com.hazelcast.core.Hazelcast;
// import com.hazelcast.core.HazelcastInstance;
//
// import org.springframework.context.annotation.Bean;
// import org.springframework.context.annotation.Configuration;
// import org.springframework.session.MapSession;
// import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
// import org.springframework.session.hazelcast.HazelcastSessionSerializer;
// import org.springframework.session.hazelcast.PrincipalNameExtractor;
// import
// org.springframework.session.hazelcast.config.annotation.web.http.EnableHazelcastHttpSession;
import com.hazelcast.config.Config;
import com.hazelcast.config.MapAttributeConfig;
import com.hazelcast.config.MapIndexConfig;
import com.hazelcast.config.SerializerConfig;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
// tag::config[]
// @EnableHazelcastHttpSession // <1>
// @Configuration
// public class HazelcastHttpSessionConfig {
//
// @Bean
// public HazelcastInstance hazelcastInstance() {
// Config config = new Config();
// AttributeConfig attributeConfig = new AttributeConfig()
// .setName(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE)
// .setExtractorClassName(PrincipalNameExtractor.class.getName());
// config.getMapConfig(HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME) // <2>
// .addAttributeConfig(attributeConfig).addIndexConfig(
// new IndexConfig(IndexType.HASH,
// HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE));
// SerializerConfig serializerConfig = new SerializerConfig();
// serializerConfig.setImplementation(new
// HazelcastSessionSerializer()).setTypeClass(MapSession.class);
// config.getSerializationConfig().addSerializerConfig(serializerConfig); // <3>
// return Hazelcast.newHazelcastInstance(config); // <4>
// }
//
// }
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.MapSession;
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
import org.springframework.session.hazelcast.HazelcastSessionSerializer;
import org.springframework.session.hazelcast.PrincipalNameExtractor;
import org.springframework.session.hazelcast.config.annotation.web.http.EnableHazelcastHttpSession;
//tag::config[]
@EnableHazelcastHttpSession // <1>
@Configuration
public class HazelcastHttpSessionConfig {
@Bean
public HazelcastInstance hazelcastInstance() {
Config config = new Config();
MapAttributeConfig attributeConfig = new MapAttributeConfig()
.setName(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE)
.setExtractor(PrincipalNameExtractor.class.getName());
config.getMapConfig(HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME) // <2>
.addMapAttributeConfig(attributeConfig).addMapIndexConfig(
new MapIndexConfig(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE, false));
SerializerConfig serializerConfig = new SerializerConfig();
serializerConfig.setImplementation(new HazelcastSessionSerializer()).setTypeClass(MapSession.class);
config.getSerializationConfig().addSerializerConfig(serializerConfig); // <3>
return Hazelcast.newHazelcastInstance(config); // <4>
}
}
// end::config[]

View File

@@ -19,7 +19,7 @@ package docs.security;
import java.time.Duration;
import java.util.Base64;
import jakarta.servlet.http.Cookie;
import javax.servlet.http.Cookie;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

View File

@@ -19,7 +19,7 @@ package docs.security;
import java.time.Duration;
import java.util.Base64;
import jakarta.servlet.http.Cookie;
import javax.servlet.http.Cookie;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

View File

@@ -14,7 +14,7 @@
<context:annotation-config/>
<bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisIndexedHttpSessionConfiguration"/>
<bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration"/>
<bean class="docs.http.AbstractHttpSessionListenerTests"
factory-method="createMockRedisConnection"/>

View File

@@ -148,78 +148,6 @@ 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-redissessionrepository]]
== Using `RedisSessionRepository`
`RedisSessionRepository` is a `SessionRepository` that is implemented by using Spring Data's `RedisOperations`.
In a web environment, this is typically used in combination with `SessionRepositoryFilter`.
Note that this implementation does not support publishing of session events.
[[api-redissessionrepository-new]]
=== Instantiating a `RedisSessionRepository`
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-redissessionrepository]
----
====
For additional information on how to create a `RedisConnectionFactory`, see the Spring Data Redis Reference.
[[api-redissessionrepository-config]]
=== Using `@EnableRedisHttpSession`
In a web environment, the simplest way to create a new `RedisSessionRepository` is to use `@EnableRedisHttpSession`.
You can find complete example usage in the xref:samples.adoc#samples[Samples and Guides (Start Here)].
You can use the following attributes to customize the configuration:
enableIndexingAndEvents
* *enableIndexingAndEvents*: Whether to use a `RedisIndexedSessionRepository` instead of a `RedisSessionRepository`. The default is `false`.
* *maxInactiveIntervalInSeconds*: The amount of time before the session expires, in seconds.
* *redisNamespace*: Allows configuring an application specific namespace for the sessions. Redis keys and channel IDs start with the prefix of `<redisNamespace>:`.
* *flushMode*: Allows specifying when data is written to Redis. The default is only when `save` is invoked on `SessionRepository`.
A value of `FlushMode.IMMEDIATE` writes to Redis as soon as possible.
==== Custom `RedisSerializer`
You can customize the serialization by creating a bean named `springSessionDefaultRedisSerializer` that implements `RedisSerializer<Object>`.
[[api-redissessionrepository-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].
For example, you can enter the following command into a terminal window:
====
[source,bash]
----
$ redis-cli
redis 127.0.0.1:6379> keys *
1) "spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021" <1>
----
<1> The suffix of this key is the session identifier of the Spring Session.
====
You can also view the attributes of each session by using the `hkeys` command.
The following example shows how to do so:
====
[source,bash]
----
redis 127.0.0.1:6379> hkeys spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021
1) "lastAccessedTime"
2) "creationTime"
3) "maxInactiveInterval"
4) "sessionAttr:username"
redis 127.0.0.1:6379> hget spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021 sessionAttr:username
"\xac\xed\x00\x05t\x00\x03rob"
----
====
[[api-redisindexedsessionrepository]]
== Using `RedisIndexedSessionRepository`
@@ -242,13 +170,12 @@ include::{indexdoc-tests}[tags=new-redisindexedsessionrepository]
For additional information on how to create a `RedisConnectionFactory`, see the Spring Data Redis Reference.
[[api-redisindexedsessionrepository-config]]
=== Using `@EnableRedisHttpSession(enableIndexingAndEvents = true)`
=== Using `@EnableRedisHttpSession`
In a web environment, the simplest way to create a new `RedisIndexedSessionRepository` is to use `@EnableRedisHttpSession(enableIndexingAndEvents = true)`.
In a web environment, the simplest way to create a new `RedisIndexedSessionRepository` is to use `@EnableRedisHttpSession`.
You can find complete example usage in the xref:samples.adoc#samples[Samples and Guides (Start Here)].
You can use the following attributes to customize the configuration:
* *enableIndexingAndEvents*: Whether to use a `RedisIndexedSessionRepository` instead of a `RedisSessionRepository`. The default is `false`.
* *maxInactiveIntervalInSeconds*: The amount of time before the session expires, in seconds.
* *redisNamespace*: Allows configuring an application specific namespace for the sessions. Redis keys and channel IDs start with the prefix of `<redisNamespace>:`.
* *flushMode*: Allows specifying when data is written to Redis. The default is only when `save` is invoked on `SessionRepository`.
@@ -408,7 +335,7 @@ redis-cli config set notify-keyspace-events Egx
----
====
If you use `@EnableRedisHttpSession(enableIndexingAndEvents = true)`, managing the `SessionMessageListener` and enabling the necessary Redis Keyspace events is done automatically.
If you use `@EnableRedisHttpSession`, managing the `SessionMessageListener` and enabling the necessary Redis Keyspace events is done automatically.
However, in a secured Redis enviornment, the config command is disabled.
This means that Spring Session cannot configure Redis Keyspace events for you.
To disable the automatic configuration, add `ConfigureRedisAction.NO_OP` as a bean.

View File

@@ -235,7 +235,7 @@ To use this support, you need to:
* Configure `SessionEventHttpSessionListenerAdapter` as a Spring bean.
* Inject every `HttpSessionListener` into the `SessionEventHttpSessionListenerAdapter`
If you use the Redis support with `enableIndexingAndEvents` set to `true`, `@EnableRedisHttpSession(enableIndexingAndEvents = true)`, all you need to do is register every `HttpSessionListener` as a bean.
If you use the configuration support documented in <<httpsession-redis,`HttpSession` with Redis>>, all you need to do is register every `HttpSessionListener` as a bean.
For example, assume you want to support Spring Security's concurrency control and need to use `HttpSessionEventPublisher`. In that case, you can add `HttpSessionEventPublisher` as a bean.
In Java configuration, this might look like the following:

View File

@@ -67,9 +67,9 @@ Spring Session is Open Source software released under the https://www.apache.org
The minimum requirements for Spring Session are:
* Java 17+.
* 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 6.0.x.
* 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 xref:api.adoc#api-redisindexedsessionrepository-expiration[Session Expiration]
* `@EnableHazelcastHttpSession` requires Hazelcast 3.6+. This is necessary to support xref:api.adoc#api-enablehazelcasthttpsession-storage[`FindByIndexNameSessionRepository`]

View File

@@ -1,20 +1,46 @@
[[upgrading-3.0]]
= Upgrading to 3.x
[[upgrading-2.0]]
= Upgrading to 2.x
With the new major release version, the Spring Session team took the opportunity to make some non-passive changes.
The focus of these changes is to improve and harmonize Spring Session's APIs as well as remove the deprecated components.
== Baseline Update
Spring Session 3.0 requires Java 17 and Spring Framework 6.0 as a baseline, since its entire codebase is now based on Java 17 source code.
Spring Session 2.0 requires Java 8 and Spring Framework 5.0 as a baseline, since its entire codebase is now based on Java 8 source code.
See https://github.com/spring-projects/spring-framework/wiki/Upgrading-to-Spring-Framework-5.x[Upgrading to Spring Framework 5.x] for more on upgrading Spring Framework.
== Replaced and Removed Modules
As a part of the project's splitting of the modules, the existing `spring-session` has been replaced with the `spring-session-core` module.
The `spring-session-core` module holds only the common set of APIs and components, while other modules contain the implementation of the appropriate `SessionRepository` and functionality related to that data store.
This applies to several existing modules that were previously a simple dependency aggregator helper module.
With new module arrangement, the following modules actually carry the implementation:
* Spring Session for MongoDB
* Spring Session for Redis
* Spring Session JDBC
* Spring Session Hazelcast
Also, the following were removed from the main project repository:
* Spring Session Data GemFire
** https://github.com/spring-projects/spring-session-data-geode[`spring-session-data-geode`]
== Replaced and Removed Packages, Classes, and Methods
The following changes were made to packages, classes, and methods:
* `ExpiringSession` API has been merged into the `Session` API.
* The `Session` API has been enhanced to make full use of Java 8.
* The `Session` API has been extended with `changeSessionId` support.
* The `SessionRepository` API has been updated to better align with Spring Data method naming conventions.
* `AbstractSessionEvent` and its subclasses are no longer constructable without an underlying `Session` object.
* The Redis namespace used by `RedisOperationsSessionRepository` is now fully configurable, instead of being partially configurable.
* Redis configuration support has been updated to avoid registering a Spring Session-specific `RedisTemplate` bean.
* JDBC configuration support has been updated to avoid registering a Spring Session-specific `JdbcTemplate` bean.
* Previously deprecated classes and methods have been removed across the codebase
== Dropped Support
As a part of the changes to `HttpSessionStrategy` and its alignment to the counterpart from the reactive world, the support for managing multiple users' sessions in a single browser instance has been removed.
The introduction of a new API to replace this functionality is under consideration for future releases.

View File

@@ -8,7 +8,7 @@ apply plugin: 'io.spring.convention.spring-test'
dependencies {
testImplementation project(':spring-session-core')
testImplementation project(':spring-session-data-redis')
// testImplementation project(':spring-session-hazelcast')
testImplementation project(':spring-session-hazelcast')
testImplementation project(':spring-session-jdbc')
testImplementation 'org.springframework:spring-jdbc'
testImplementation 'org.springframework:spring-messaging'
@@ -22,7 +22,6 @@ dependencies {
testImplementation 'org.assertj:assertj-core'
testImplementation 'com.hazelcast:hazelcast'
testImplementation 'io.lettuce:lettuce-core'
testImplementation 'jakarta.annotation:jakarta.annotation-api'
testImplementation 'jakarta.servlet:jakarta.servlet-api'
testImplementation 'org.junit.jupiter:junit-jupiter-api'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'

View File

@@ -19,7 +19,7 @@ package docs.security;
import java.time.Duration;
import java.util.Base64;
import jakarta.servlet.http.Cookie;
import javax.servlet.http.Cookie;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

View File

@@ -19,7 +19,7 @@ package docs.security;
import java.time.Duration;
import java.util.Base64;
import jakarta.servlet.http.Cookie;
import javax.servlet.http.Cookie;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

View File

@@ -0,0 +1,41 @@
plugins {
id 'java-library'
id 'io.spring.convention.repository'
id 'io.spring.convention.springdependencymangement'
id 'io.spring.convention.checkstyle'
id 'io.spring.convention.tests-configuration'
id 'io.spring.convention.integration-test'
id 'org.springframework.propdeps'
}
configurations {
classesOnlyElements {
canBeConsumed = true
canBeResolved = false
}
}
artifacts {
classesOnlyElements(compileJava.destinationDir)
}
dependencies {
api project(':spring-session-core')
optional "com.hazelcast:hazelcast:4.2.4"
api "org.springframework:spring-context"
api "jakarta.annotation:jakarta.annotation-api"
testImplementation "jakarta.servlet:jakarta.servlet-api"
testImplementation "org.assertj:assertj-core"
testImplementation "org.mockito:mockito-core"
testImplementation "org.springframework:spring-test"
testImplementation "org.springframework:spring-web"
testImplementation "org.junit.jupiter:junit-jupiter-api"
testImplementation "org.springframework.security:spring-security-core"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine"
integrationTestCompile "org.testcontainers:testcontainers"
integrationTestCompile "com.hazelcast:hazelcast:4.2.4"
integrationTestCompile project(":spring-session-hazelcast")
}

View File

@@ -0,0 +1,251 @@
/*
* Copyright 2014-2021 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 com.hazelcast.core.HazelcastInstance;
import com.hazelcast.map.IMap;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.session.MapSession;
import org.springframework.session.hazelcast.Hazelcast4IndexedSessionRepository.HazelcastSession;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Base class for {@link Hazelcast4IndexedSessionRepository} integration tests.
*
* @author Eleftheria Stein
*/
abstract class AbstractHazelcast4IndexedSessionRepositoryITests {
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
@Autowired
private HazelcastInstance hazelcastInstance;
@Autowired
private Hazelcast4IndexedSessionRepository repository;
@Test
void createAndDestroySession() {
HazelcastSession sessionToSave = this.repository.createSession();
String sessionId = sessionToSave.getId();
IMap<String, MapSession> hazelcastMap = this.hazelcastInstance
.getMap(Hazelcast4IndexedSessionRepository.DEFAULT_SESSION_MAP_NAME);
this.repository.save(sessionToSave);
assertThat(hazelcastMap.get(sessionId)).isEqualTo(sessionToSave);
this.repository.deleteById(sessionId);
assertThat(hazelcastMap.get(sessionId)).isNull();
}
@Test
void changeSessionIdWhenOnlyChangeId() {
String attrName = "changeSessionId";
String attrValue = "changeSessionId-value";
HazelcastSession toSave = this.repository.createSession();
toSave.setAttribute(attrName, attrValue);
this.repository.save(toSave);
HazelcastSession findById = this.repository.findById(toSave.getId());
assertThat(findById.<String>getAttribute(attrName)).isEqualTo(attrValue);
String originalFindById = findById.getId();
String changeSessionId = findById.changeSessionId();
this.repository.save(findById);
assertThat(this.repository.findById(originalFindById)).isNull();
HazelcastSession findByChangeSessionId = this.repository.findById(changeSessionId);
assertThat(findByChangeSessionId.<String>getAttribute(attrName)).isEqualTo(attrValue);
this.repository.deleteById(changeSessionId);
}
@Test
void changeSessionIdWhenChangeTwice() {
HazelcastSession toSave = this.repository.createSession();
this.repository.save(toSave);
String originalId = toSave.getId();
String changeId1 = toSave.changeSessionId();
String changeId2 = toSave.changeSessionId();
this.repository.save(toSave);
assertThat(this.repository.findById(originalId)).isNull();
assertThat(this.repository.findById(changeId1)).isNull();
assertThat(this.repository.findById(changeId2)).isNotNull();
this.repository.deleteById(changeId2);
}
@Test
void changeSessionIdWhenSetAttributeOnChangedSession() {
String attrName = "changeSessionId";
String attrValue = "changeSessionId-value";
HazelcastSession toSave = this.repository.createSession();
this.repository.save(toSave);
HazelcastSession findById = this.repository.findById(toSave.getId());
findById.setAttribute(attrName, attrValue);
String originalFindById = findById.getId();
String changeSessionId = findById.changeSessionId();
this.repository.save(findById);
assertThat(this.repository.findById(originalFindById)).isNull();
HazelcastSession findByChangeSessionId = this.repository.findById(changeSessionId);
assertThat(findByChangeSessionId.<String>getAttribute(attrName)).isEqualTo(attrValue);
this.repository.deleteById(changeSessionId);
}
@Test
void changeSessionIdWhenHasNotSaved() {
HazelcastSession toSave = this.repository.createSession();
String originalId = toSave.getId();
toSave.changeSessionId();
this.repository.save(toSave);
assertThat(this.repository.findById(toSave.getId())).isNotNull();
assertThat(this.repository.findById(originalId)).isNull();
this.repository.deleteById(toSave.getId());
}
@Test // gh-1076
void attemptToUpdateSessionAfterDelete() {
HazelcastSession session = this.repository.createSession();
String sessionId = session.getId();
this.repository.save(session);
session = this.repository.findById(sessionId);
session.setAttribute("attributeName", "attributeValue");
this.repository.deleteById(sessionId);
this.repository.save(session);
assertThat(this.repository.findById(sessionId)).isNull();
}
@Test
void createAndUpdateSession() {
HazelcastSession session = this.repository.createSession();
String sessionId = session.getId();
this.repository.save(session);
session = this.repository.findById(sessionId);
session.setAttribute("attributeName", "attributeValue");
this.repository.save(session);
assertThat(this.repository.findById(sessionId)).isNotNull();
this.repository.deleteById(sessionId);
}
@Test
void createSessionWithSecurityContextAndFindById() {
HazelcastSession session = this.repository.createSession();
String sessionId = session.getId();
Authentication authentication = new UsernamePasswordAuthenticationToken("saves-" + System.currentTimeMillis(),
"password", AuthorityUtils.createAuthorityList("ROLE_USER"));
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(authentication);
session.setAttribute(SPRING_SECURITY_CONTEXT, securityContext);
this.repository.save(session);
assertThat(this.repository.findById(sessionId)).isNotNull();
this.repository.deleteById(sessionId);
}
@Test
void createAndUpdateSessionWhileKeepingOriginalTimeToLiveConfiguredOnRepository() {
final Duration defaultSessionTimeout = Duration.ofSeconds(1800);
final IMap<String, MapSession> hazelcastMap = this.hazelcastInstance
.getMap(Hazelcast4IndexedSessionRepository.DEFAULT_SESSION_MAP_NAME);
HazelcastSession session = this.repository.createSession();
String sessionId = session.getId();
this.repository.save(session);
assertThat(session.getMaxInactiveInterval()).isEqualTo(defaultSessionTimeout);
assertThat(hazelcastMap.getEntryView(sessionId).getTtl()).isEqualTo(defaultSessionTimeout.toMillis());
session = this.repository.findById(sessionId);
session.setLastAccessedTime(Instant.now());
this.repository.save(session);
session = this.repository.findById(sessionId);
assertThat(session.getMaxInactiveInterval()).isEqualTo(defaultSessionTimeout);
assertThat(hazelcastMap.getEntryView(sessionId).getTtl()).isEqualTo(defaultSessionTimeout.toMillis());
}
@Test
void createAndUpdateSessionWhileKeepingTimeToLiveSetOnSession() {
final Duration individualSessionTimeout = Duration.ofSeconds(23);
final IMap<String, MapSession> hazelcastMap = this.hazelcastInstance
.getMap(Hazelcast4IndexedSessionRepository.DEFAULT_SESSION_MAP_NAME);
HazelcastSession session = this.repository.createSession();
session.setMaxInactiveInterval(individualSessionTimeout);
String sessionId = session.getId();
this.repository.save(session);
assertThat(session.getMaxInactiveInterval()).isEqualTo(individualSessionTimeout);
assertThat(hazelcastMap.getEntryView(sessionId).getTtl()).isEqualTo(individualSessionTimeout.toMillis());
session = this.repository.findById(sessionId);
session.setAttribute("attribute", "value");
this.repository.save(session);
session = this.repository.findById(sessionId);
assertThat(session.getMaxInactiveInterval()).isEqualTo(individualSessionTimeout);
assertThat(hazelcastMap.getEntryView(sessionId).getTtl()).isEqualTo(individualSessionTimeout.toMillis());
}
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2014-2022 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 com.hazelcast.client.HazelcastClient;
import com.hazelcast.client.config.ClientConfig;
import com.hazelcast.core.HazelcastInstance;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.extension.ExtendWith;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.MountableFile;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.MapSession;
import org.springframework.session.Session;
import org.springframework.session.hazelcast.config.annotation.web.http.EnableHazelcastHttpSession;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
/**
* Integration tests for {@link Hazelcast4IndexedSessionRepository} using client-server
* topology.
*
* @author Eleftheria Stein
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
class ClientServerHazelcast4IndexedSessionRepositoryITests extends AbstractHazelcast4IndexedSessionRepositoryITests {
private static GenericContainer container = new GenericContainer<>("hazelcast/hazelcast:4.2.4")
.withExposedPorts(5701).withCopyFileToContainer(MountableFile.forClasspathResource("/hazelcast-server.xml"),
"/opt/hazelcast/hazelcast.xml");
@BeforeAll
static void setUpClass() {
container.start();
}
@AfterAll
static void tearDownClass() {
container.stop();
}
@Configuration
@EnableHazelcastHttpSession
static class HazelcastSessionConfig {
@Bean
HazelcastInstance hazelcastInstance() {
ClientConfig clientConfig = new ClientConfig();
clientConfig.getNetworkConfig()
.addAddress(container.getContainerIpAddress() + ":" + container.getFirstMappedPort());
clientConfig.getUserCodeDeploymentConfig().setEnabled(true).addClass(Session.class)
.addClass(MapSession.class).addClass(Hazelcast4SessionUpdateEntryProcessor.class);
return HazelcastClient.newHazelcastClient(clientConfig);
}
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2014-2020 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 com.hazelcast.core.HazelcastInstance;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.hazelcast.config.annotation.web.http.EnableHazelcastHttpSession;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
/**
* Integration tests for {@link Hazelcast4IndexedSessionRepository} using embedded
* topology.
*
* @author Eleftheria Stein
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
class EmbeddedHazelcast4IndexedSessionRepositoryITests extends AbstractHazelcast4IndexedSessionRepositoryITests {
@EnableHazelcastHttpSession
@Configuration
static class HazelcastSessionConfig {
@Bean
HazelcastInstance hazelcastInstance() {
return Hazelcast4ITestUtils.embeddedHazelcastServer();
}
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2014-2021 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.util.Map;
import com.hazelcast.core.HazelcastInstance;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.session.FlushMode;
import org.springframework.session.hazelcast.config.annotation.web.http.EnableHazelcastHttpSession;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link Hazelcast4IndexedSessionRepository} using embedded
* topology, with flush mode set to immediate.
*
* @author Eleftheria Stein
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
class FlushImmediateHazelcast4IndexedSessionRepositoryITests {
@Autowired
private Hazelcast4IndexedSessionRepository repository;
@Test
void createSessionWithSecurityContextAndFindByPrincipalName() {
String username = "saves-" + System.currentTimeMillis();
Hazelcast4IndexedSessionRepository.HazelcastSession session = this.repository.createSession();
String sessionId = session.getId();
Authentication authentication = new UsernamePasswordAuthenticationToken(username, "password",
AuthorityUtils.createAuthorityList("ROLE_USER"));
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(authentication);
session.setAttribute("SPRING_SECURITY_CONTEXT", securityContext);
this.repository.save(session);
Map<String, Hazelcast4IndexedSessionRepository.HazelcastSession> findByPrincipalName = this.repository
.findByPrincipalName(username);
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(sessionId);
this.repository.deleteById(sessionId);
}
@EnableHazelcastHttpSession(flushMode = FlushMode.IMMEDIATE)
@Configuration
static class HazelcastSessionConfig {
@Bean
HazelcastInstance hazelcastInstance() {
return Hazelcast4ITestUtils.embeddedHazelcastServer();
}
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2014-2020 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 com.hazelcast.config.AttributeConfig;
import com.hazelcast.config.Config;
import com.hazelcast.config.IndexConfig;
import com.hazelcast.config.IndexType;
import com.hazelcast.config.NetworkConfig;
import com.hazelcast.config.SerializerConfig;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import org.springframework.session.MapSession;
/**
* Utility class for Hazelcast integration tests.
*
* @author Eleftheria Stein
*/
final class Hazelcast4ITestUtils {
private Hazelcast4ITestUtils() {
}
/**
* Creates {@link HazelcastInstance} for use in integration tests.
* @return the Hazelcast instance
*/
static HazelcastInstance embeddedHazelcastServer() {
Config config = new Config();
NetworkConfig networkConfig = config.getNetworkConfig();
networkConfig.setPort(0);
networkConfig.getJoin().getMulticastConfig().setEnabled(false);
AttributeConfig attributeConfig = new AttributeConfig()
.setName(Hazelcast4IndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE)
.setExtractorClassName(Hazelcast4PrincipalNameExtractor.class.getName());
config.getMapConfig(Hazelcast4IndexedSessionRepository.DEFAULT_SESSION_MAP_NAME)
.addAttributeConfig(attributeConfig).addIndexConfig(
new IndexConfig(IndexType.HASH, Hazelcast4IndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE));
SerializerConfig serializerConfig = new SerializerConfig();
serializerConfig.setImplementation(new HazelcastSessionSerializer()).setTypeClass(MapSession.class);
config.getSerializationConfig().addSerializerConfig(serializerConfig);
return Hazelcast.newHazelcastInstance(config);
}
}

View File

@@ -0,0 +1,241 @@
/*
* Copyright 2014-2021 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 com.hazelcast.core.HazelcastInstance;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDeletedEvent;
import org.springframework.session.events.SessionExpiredEvent;
import org.springframework.session.hazelcast.config.annotation.web.http.EnableHazelcastHttpSession;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Ensure that the appropriate SessionEvents are fired at the expected times. Additionally
* ensure that the interactions with the {@link SessionRepository} abstraction behave as
* expected after each SessionEvent.
*
* @author Eleftheria Stein
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
class SessionEventHazelcast4IndexedSessionRepositoryTests<S extends Session> {
private static final int MAX_INACTIVE_INTERVAL_IN_SECONDS = 2;
@Autowired
private SessionRepository<S> repository;
@Autowired
private SessionEventRegistry registry;
@BeforeEach
void setup() {
this.registry.clear();
}
@Test
void saveSessionTest() throws InterruptedException {
String username = "saves-" + System.currentTimeMillis();
S sessionToSave = this.repository.createSession();
String expectedAttributeName = "a";
String expectedAttributeValue = "b";
sessionToSave.setAttribute(expectedAttributeName, expectedAttributeValue);
Authentication toSaveToken = new UsernamePasswordAuthenticationToken(username, "password",
AuthorityUtils.createAuthorityList("ROLE_USER"));
SecurityContext toSaveContext = SecurityContextHolder.createEmptyContext();
toSaveContext.setAuthentication(toSaveToken);
sessionToSave.setAttribute("SPRING_SECURITY_CONTEXT", toSaveContext);
sessionToSave.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, username);
this.repository.save(sessionToSave);
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isTrue();
assertThat(this.registry.<SessionCreatedEvent>getEvent(sessionToSave.getId()))
.isInstanceOf(SessionCreatedEvent.class);
Session session = this.repository.findById(sessionToSave.getId());
assertThat(session.getId()).isEqualTo(sessionToSave.getId());
assertThat(session.getAttributeNames()).isEqualTo(sessionToSave.getAttributeNames());
assertThat(session.<String>getAttribute(expectedAttributeName))
.isEqualTo(sessionToSave.getAttribute(expectedAttributeName));
}
@Test
void expiredSessionTest() throws InterruptedException {
S sessionToSave = this.repository.createSession();
this.repository.save(sessionToSave);
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isTrue();
assertThat(this.registry.<SessionCreatedEvent>getEvent(sessionToSave.getId()))
.isInstanceOf(SessionCreatedEvent.class);
this.registry.clear();
assertThat(sessionToSave.getMaxInactiveInterval())
.isEqualTo(Duration.ofSeconds(MAX_INACTIVE_INTERVAL_IN_SECONDS));
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isTrue();
assertThat(this.registry.<SessionExpiredEvent>getEvent(sessionToSave.getId()))
.isInstanceOf(SessionExpiredEvent.class);
assertThat(this.repository.findById(sessionToSave.getId())).isNull();
}
@Test
void deletedSessionTest() throws InterruptedException {
S sessionToSave = this.repository.createSession();
this.repository.save(sessionToSave);
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isTrue();
assertThat(this.registry.<SessionCreatedEvent>getEvent(sessionToSave.getId()))
.isInstanceOf(SessionCreatedEvent.class);
this.registry.clear();
this.repository.deleteById(sessionToSave.getId());
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isTrue();
assertThat(this.registry.<SessionDeletedEvent>getEvent(sessionToSave.getId()))
.isInstanceOf(SessionDeletedEvent.class);
assertThat(this.repository.findById(sessionToSave.getId())).isNull();
}
@Test
void saveUpdatesTimeToLiveTest() throws InterruptedException {
S sessionToSave = this.repository.createSession();
sessionToSave.setMaxInactiveInterval(Duration.ofSeconds(3));
this.repository.save(sessionToSave);
Thread.sleep(2000);
// Get and save the session like SessionRepositoryFilter would.
S sessionToUpdate = this.repository.findById(sessionToSave.getId());
sessionToUpdate.setLastAccessedTime(Instant.now());
this.repository.save(sessionToUpdate);
Thread.sleep(2000);
assertThat(this.repository.findById(sessionToUpdate.getId())).isNotNull();
}
@Test // gh-1077
void changeSessionIdNoEventTest() throws InterruptedException {
S sessionToSave = this.repository.createSession();
sessionToSave.setMaxInactiveInterval(Duration.ofMinutes(30));
this.repository.save(sessionToSave);
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isTrue();
assertThat(this.registry.<SessionCreatedEvent>getEvent(sessionToSave.getId()))
.isInstanceOf(SessionCreatedEvent.class);
this.registry.clear();
sessionToSave.changeSessionId();
this.repository.save(sessionToSave);
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isFalse();
}
@Test // gh-1300
void updateMaxInactiveIntervalTest() throws InterruptedException {
S sessionToSave = this.repository.createSession();
sessionToSave.setMaxInactiveInterval(Duration.ofMinutes(30));
this.repository.save(sessionToSave);
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isTrue();
assertThat(this.registry.<SessionCreatedEvent>getEvent(sessionToSave.getId()))
.isInstanceOf(SessionCreatedEvent.class);
this.registry.clear();
S sessionToUpdate = this.repository.findById(sessionToSave.getId());
sessionToUpdate.setLastAccessedTime(Instant.now());
sessionToUpdate.setMaxInactiveInterval(Duration.ofSeconds(1));
this.repository.save(sessionToUpdate);
assertThat(this.registry.receivedEvent(sessionToUpdate.getId())).isTrue();
assertThat(this.registry.<SessionExpiredEvent>getEvent(sessionToUpdate.getId()))
.isInstanceOf(SessionExpiredEvent.class);
assertThat(this.repository.findById(sessionToUpdate.getId())).isNull();
}
@Test // gh-1899
void updateSessionAndExpireAfterOriginalTimeToLiveTest() throws InterruptedException {
S sessionToSave = this.repository.createSession();
this.repository.save(sessionToSave);
assertThat(this.registry.receivedEvent(sessionToSave.getId())).isTrue();
assertThat(this.registry.<SessionCreatedEvent>getEvent(sessionToSave.getId()))
.isInstanceOf(SessionCreatedEvent.class);
this.registry.clear();
S sessionToUpdate = this.repository.findById(sessionToSave.getId());
sessionToUpdate.setLastAccessedTime(Instant.now());
this.repository.save(sessionToUpdate);
assertThat(this.registry.receivedEvent(sessionToUpdate.getId())).isTrue();
assertThat(this.registry.<SessionExpiredEvent>getEvent(sessionToUpdate.getId()))
.isInstanceOf(SessionExpiredEvent.class);
// Assert this after the expired event was received because it would otherwise do
// its own expiration check and explicitly delete the session from Hazelcast
// regardless of the TTL of the IMap entry.
assertThat(this.repository.findById(sessionToUpdate.getId())).isNull();
}
@Configuration
@EnableHazelcastHttpSession(maxInactiveIntervalInSeconds = MAX_INACTIVE_INTERVAL_IN_SECONDS)
static class HazelcastSessionConfig {
@Bean
HazelcastInstance embeddedHazelcast() {
return Hazelcast4ITestUtils.embeddedHazelcastServer();
}
@Bean
SessionEventRegistry sessionEventRegistry() {
return new SessionEventRegistry();
}
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2014-2020 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.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.springframework.context.ApplicationListener;
import org.springframework.session.events.AbstractSessionEvent;
class SessionEventRegistry implements ApplicationListener<AbstractSessionEvent> {
private Map<String, AbstractSessionEvent> events = new HashMap<>();
private ConcurrentMap<String, Object> locks = new ConcurrentHashMap<>();
@Override
public void onApplicationEvent(AbstractSessionEvent event) {
String sessionId = event.getSessionId();
this.events.put(sessionId, event);
Object lock = getLock(sessionId);
synchronized (lock) {
lock.notifyAll();
}
}
void clear() {
this.events.clear();
this.locks.clear();
}
boolean receivedEvent(String sessionId) throws InterruptedException {
return waitForEvent(sessionId) != null;
}
@SuppressWarnings("unchecked")
<E extends AbstractSessionEvent> E getEvent(String sessionId) throws InterruptedException {
return (E) waitForEvent(sessionId);
}
@SuppressWarnings("unchecked")
private <E extends AbstractSessionEvent> E waitForEvent(String sessionId) throws InterruptedException {
Object lock = getLock(sessionId);
synchronized (lock) {
if (!this.events.containsKey(sessionId)) {
lock.wait(10000);
}
}
return (E) this.events.get(sessionId);
}
private Object getLock(String sessionId) {
return this.locks.computeIfAbsent(sessionId, (k) -> new Object());
}
}

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<hazelcast xmlns="http://www.hazelcast.com/schema/config"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.hazelcast.com/schema/config https://www.hazelcast.com/schema/config/hazelcast-config-4.2.xsd">
<network>
<join>
<multicast enabled="false"/>
</join>
</network>
<user-code-deployment enabled="true">
<class-cache-mode>ETERNAL</class-cache-mode>
<provider-mode>LOCAL_AND_CACHED_CLASSES</provider-mode>
</user-code-deployment>
</hazelcast>

View File

@@ -0,0 +1 @@
ryuk.container.timeout=120

View File

@@ -0,0 +1,478 @@
/*
* Copyright 2014-2021 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.UUID;
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.map.IMap;
import com.hazelcast.map.listener.EntryAddedListener;
import com.hazelcast.map.listener.EntryEvictedListener;
import com.hazelcast.map.listener.EntryExpiredListener;
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;
/**
* A {@link org.springframework.session.SessionRepository} implementation using Hazelcast
* 4 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);
*
* Hazelcast4IndexedSessionRepository sessionRepository =
* new Hazelcast4IndexedSessionRepository(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">
* AttributeConfig attributeConfig = new AttributeConfig()
* .setName(Hazelcast4IndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE)
* .setExtractorClassName(Hazelcast4PrincipalNameExtractor.class.getName());
*
* Config config = new Config();
*
* config.getMapConfig(Hazelcast4IndexedSessionRepository.DEFAULT_SESSION_MAP_NAME)
* .addAttributeConfig(attributeConfig)
* .addIndexConfig(new IndexConfig(
* IndexType.HASH,
* Hazelcast4IndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE));
*
* 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>entryExpired - {@link SessionExpiredEvent}</li>
* <li>entryRemoved - {@link SessionDeletedEvent}</li>
* </ul>
*
* @author Eleftheria Stein
* @since 2.4.0
*/
public class Hazelcast4IndexedSessionRepository
implements FindByIndexNameSessionRepository<Hazelcast4IndexedSessionRepository.HazelcastSession>,
EntryAddedListener<String, MapSession>, EntryEvictedListener<String, MapSession>,
EntryRemovedListener<String, MapSession>, EntryExpiredListener<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 Log logger = LogFactory.getLog(Hazelcast4IndexedSessionRepository.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 UUID sessionListenerId;
/**
* Create a new {@link Hazelcast4IndexedSessionRepository} instance.
* @param hazelcastInstance the {@link HazelcastInstance} to use for managing sessions
*/
public Hazelcast4IndexedSessionRepository(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()) {
Hazelcast4SessionUpdateEntryProcessor entryProcessor = new Hazelcast4SessionUpdateEntryProcessor();
if (session.lastAccessedTimeChanged) {
entryProcessor.setLastAccessedTime(session.getLastAccessedTime());
}
if (session.maxInactiveIntervalChanged) {
entryProcessor.setMaxInactiveInterval(session.getMaxInactiveInterval());
}
if (!session.delta.isEmpty()) {
entryProcessor.setDelta(new HashMap<>(session.delta));
}
this.sessions.executeOnKey(session.getId(), entryProcessor);
}
session.clearChangeFlags();
}
@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));
}
}
@Override
public void entryExpired(EntryEvent<String, MapSession> event) {
if (logger.isDebugEnabled()) {
logger.debug("Session expired with id: " + event.getOldValue().getId());
}
this.eventPublisher.publishEvent(new SessionExpiredEvent(this, event.getOldValue()));
}
/**
* A custom implementation of {@link Session} that uses a {@link MapSession} as the
* basis for its mapping. It keeps track if changes have been made since last save.
*
* @author Aleksandar Stojsavljevic
*/
final class HazelcastSession implements Session {
private final MapSession delegate;
private boolean 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 || (Hazelcast4IndexedSessionRepository.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) {
Assert.notNull(interval, "interval must not be null");
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
&& Hazelcast4IndexedSessionRepository.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 = Hazelcast4IndexedSessionRepository.this.indexResolver
.resolveIndexesFor(this);
String principal = (attributeValue != null) ? indexes.get(PRINCIPAL_NAME_INDEX_NAME) : null;
this.delegate.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal);
this.delta.put(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 (Hazelcast4IndexedSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {
Hazelcast4IndexedSessionRepository.this.save(this);
}
}
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2014-2020 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 com.hazelcast.query.extractor.ValueCollector;
import com.hazelcast.query.extractor.ValueExtractor;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.MapSession;
/**
* Hazelcast {@link ValueExtractor} responsible for extracting principal name from the
* {@link MapSession} to be used with Hazelcast 4.
*
* @author Eleftheria Stein
* @since 2.4.0
*/
public class Hazelcast4PrincipalNameExtractor implements ValueExtractor<MapSession, String> {
@Override
@SuppressWarnings("unchecked")
public void extract(MapSession target, String argument, ValueCollector collector) {
String principalName = target.getAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
if (principalName != null) {
collector.addObject(principalName);
}
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2014-2021 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.Map;
import java.util.concurrent.TimeUnit;
import com.hazelcast.map.EntryProcessor;
import com.hazelcast.map.ExtendedMapEntry;
import org.springframework.session.MapSession;
/**
* Hazelcast {@link EntryProcessor} responsible for handling updates to session when using
* Hazelcast 4.
*
* @author Eleftheria Stein
* @since 2.4.0
*/
public class Hazelcast4SessionUpdateEntryProcessor implements EntryProcessor<String, MapSession, Object> {
private Instant lastAccessedTime;
private Duration maxInactiveInterval;
private Map<String, Object> delta;
@Override
public Object process(Map.Entry<String, MapSession> entry) {
MapSession value = entry.getValue();
if (value == null) {
return Boolean.FALSE;
}
if (this.lastAccessedTime != null) {
value.setLastAccessedTime(this.lastAccessedTime);
}
if (this.maxInactiveInterval != null) {
value.setMaxInactiveInterval(this.maxInactiveInterval);
}
if (this.delta != null) {
for (final Map.Entry<String, Object> attribute : this.delta.entrySet()) {
if (attribute.getValue() != null) {
value.setAttribute(attribute.getKey(), attribute.getValue());
}
else {
value.removeAttribute(attribute.getKey());
}
}
}
((ExtendedMapEntry<String, MapSession>) entry).setValue(value, value.getMaxInactiveInterval().getSeconds(),
TimeUnit.SECONDS);
return Boolean.TRUE;
}
void setLastAccessedTime(Instant lastAccessedTime) {
this.lastAccessedTime = lastAccessedTime;
}
void setMaxInactiveInterval(Duration maxInactiveInterval) {
this.maxInactiveInterval = maxInactiveInterval;
}
void setDelta(Map<String, Object> delta) {
this.delta = delta;
}
}

View File

@@ -0,0 +1,470 @@
/*
* Copyright 2014-2021 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.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.map.EntryProcessor;
import com.hazelcast.map.IMap;
import com.hazelcast.map.listener.MapListener;
import com.hazelcast.query.impl.predicates.EqualPredicate;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.FlushMode;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.hazelcast.Hazelcast4IndexedSessionRepository.HazelcastSession;
import org.springframework.test.util.ReflectionTestUtils;
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.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
/**
* Tests for {@link Hazelcast4IndexedSessionRepository}.
*
* @author Eleftheria Stein
*/
class Hazelcast4IndexedSessionRepositoryTests {
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
private HazelcastInstance hazelcastInstance = mock(HazelcastInstance.class);
@SuppressWarnings("unchecked")
private IMap<String, MapSession> sessions = mock(IMap.class);
private Hazelcast4IndexedSessionRepository repository;
@BeforeEach
void setUp() {
given(this.hazelcastInstance.<String, MapSession>getMap(anyString())).willReturn(this.sessions);
this.repository = new Hazelcast4IndexedSessionRepository(this.hazelcastInstance);
this.repository.init();
}
@Test
void constructorNullHazelcastInstance() {
assertThatIllegalArgumentException().isThrownBy(() -> new Hazelcast4IndexedSessionRepository(null))
.withMessage("HazelcastInstance must not be null");
}
@Test
void setSaveModeNull() {
assertThatIllegalArgumentException().isThrownBy(() -> this.repository.setSaveMode(null))
.withMessage("saveMode must not be null");
}
@Test
void createSessionDefaultMaxInactiveInterval() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
HazelcastSession session = this.repository.createSession();
assertThat(session.getMaxInactiveInterval()).isEqualTo(new MapSession().getMaxInactiveInterval());
verifyNoMoreInteractions(this.sessions);
}
@Test
void createSessionCustomMaxInactiveInterval() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
int interval = 1;
this.repository.setDefaultMaxInactiveInterval(interval);
HazelcastSession session = this.repository.createSession();
assertThat(session.getMaxInactiveInterval()).isEqualTo(Duration.ofSeconds(interval));
verifyNoMoreInteractions(this.sessions);
}
@Test
void saveNewFlushModeOnSave() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
HazelcastSession session = this.repository.createSession();
verifyNoMoreInteractions(this.sessions);
this.repository.save(session);
verify(this.sessions, times(1)).set(eq(session.getId()), eq(session.getDelegate()), isA(Long.class),
eq(TimeUnit.SECONDS));
verifyNoMoreInteractions(this.sessions);
}
@Test
void saveNewFlushModeImmediate() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
this.repository.setFlushMode(FlushMode.IMMEDIATE);
HazelcastSession session = this.repository.createSession();
verify(this.sessions, times(1)).set(eq(session.getId()), eq(session.getDelegate()), isA(Long.class),
eq(TimeUnit.SECONDS));
verifyNoMoreInteractions(this.sessions);
}
@Test
void saveUpdatedAttributeFlushModeOnSave() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
HazelcastSession session = this.repository.createSession();
session.setAttribute("testName", "testValue");
verifyNoMoreInteractions(this.sessions);
this.repository.save(session);
verify(this.sessions, times(1)).set(eq(session.getId()), eq(session.getDelegate()), isA(Long.class),
eq(TimeUnit.SECONDS));
verifyNoMoreInteractions(this.sessions);
}
@Test
void saveUpdatedAttributeFlushModeImmediate() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
this.repository.setFlushMode(FlushMode.IMMEDIATE);
HazelcastSession session = this.repository.createSession();
session.setAttribute("testName", "testValue");
verify(this.sessions, times(1)).set(eq(session.getId()), eq(session.getDelegate()), isA(Long.class),
eq(TimeUnit.SECONDS));
verify(this.sessions, times(1)).executeOnKey(eq(session.getId()), any(EntryProcessor.class));
this.repository.save(session);
verifyNoMoreInteractions(this.sessions);
}
@Test
void removeAttributeFlushModeOnSave() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
HazelcastSession session = this.repository.createSession();
session.removeAttribute("testName");
verifyNoMoreInteractions(this.sessions);
this.repository.save(session);
verify(this.sessions, times(1)).set(eq(session.getId()), eq(session.getDelegate()), isA(Long.class),
eq(TimeUnit.SECONDS));
verifyNoMoreInteractions(this.sessions);
}
@Test
void removeAttributeFlushModeImmediate() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
this.repository.setFlushMode(FlushMode.IMMEDIATE);
HazelcastSession session = this.repository.createSession();
session.removeAttribute("testName");
verify(this.sessions, times(1)).set(eq(session.getId()), eq(session.getDelegate()), isA(Long.class),
eq(TimeUnit.SECONDS));
verify(this.sessions, times(1)).executeOnKey(eq(session.getId()), any(EntryProcessor.class));
this.repository.save(session);
verifyNoMoreInteractions(this.sessions);
}
@Test
void saveUpdatedLastAccessedTimeFlushModeOnSave() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
HazelcastSession session = this.repository.createSession();
session.setLastAccessedTime(Instant.now());
verifyNoMoreInteractions(this.sessions);
this.repository.save(session);
verify(this.sessions, times(1)).set(eq(session.getId()), eq(session.getDelegate()), isA(Long.class),
eq(TimeUnit.SECONDS));
verifyNoMoreInteractions(this.sessions);
}
@Test
void saveUpdatedLastAccessedTimeFlushModeImmediate() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
this.repository.setFlushMode(FlushMode.IMMEDIATE);
HazelcastSession session = this.repository.createSession();
session.setLastAccessedTime(Instant.now());
verify(this.sessions, times(1)).set(eq(session.getId()), eq(session.getDelegate()), isA(Long.class),
eq(TimeUnit.SECONDS));
verify(this.sessions, times(1)).executeOnKey(eq(session.getId()), any(EntryProcessor.class));
this.repository.save(session);
verifyNoMoreInteractions(this.sessions);
}
@Test
void saveUpdatedMaxInactiveIntervalInSecondsFlushModeOnSave() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
HazelcastSession session = this.repository.createSession();
session.setMaxInactiveInterval(Duration.ofSeconds(1));
verifyNoMoreInteractions(this.sessions);
this.repository.save(session);
verify(this.sessions, times(1)).set(eq(session.getId()), eq(session.getDelegate()), isA(Long.class),
eq(TimeUnit.SECONDS));
verifyNoMoreInteractions(this.sessions);
}
@Test
void saveUpdatedMaxInactiveIntervalInSecondsFlushModeImmediate() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
this.repository.setFlushMode(FlushMode.IMMEDIATE);
HazelcastSession session = this.repository.createSession();
String sessionId = session.getId();
session.setMaxInactiveInterval(Duration.ofSeconds(1));
verify(this.sessions, times(1)).set(eq(sessionId), eq(session.getDelegate()), isA(Long.class),
eq(TimeUnit.SECONDS));
verify(this.sessions, times(1)).executeOnKey(eq(sessionId), any(EntryProcessor.class));
this.repository.save(session);
verifyNoMoreInteractions(this.sessions);
}
@Test
void saveUnchangedFlushModeOnSave() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
HazelcastSession session = this.repository.createSession();
this.repository.save(session);
verify(this.sessions, times(1)).set(eq(session.getId()), eq(session.getDelegate()), isA(Long.class),
eq(TimeUnit.SECONDS));
this.repository.save(session);
verifyNoMoreInteractions(this.sessions);
}
@Test
void saveUnchangedFlushModeImmediate() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
this.repository.setFlushMode(FlushMode.IMMEDIATE);
HazelcastSession session = this.repository.createSession();
verify(this.sessions, times(1)).set(eq(session.getId()), eq(session.getDelegate()), isA(Long.class),
eq(TimeUnit.SECONDS));
this.repository.save(session);
verifyNoMoreInteractions(this.sessions);
}
@Test
void getSessionNotFound() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
String sessionId = "testSessionId";
HazelcastSession session = this.repository.findById(sessionId);
assertThat(session).isNull();
verify(this.sessions, times(1)).get(eq(sessionId));
verifyNoMoreInteractions(this.sessions);
}
@Test
void getSessionExpired() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
MapSession expired = new MapSession();
expired.setLastAccessedTime(Instant.now().minusSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS + 1));
given(this.sessions.get(eq(expired.getId()))).willReturn(expired);
HazelcastSession session = this.repository.findById(expired.getId());
assertThat(session).isNull();
verify(this.sessions, times(1)).get(eq(expired.getId()));
verify(this.sessions, times(1)).remove(eq(expired.getId()));
verifyNoMoreInteractions(this.sessions);
}
@Test
void getSessionFound() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
MapSession saved = new MapSession();
saved.setAttribute("savedName", "savedValue");
given(this.sessions.get(eq(saved.getId()))).willReturn(saved);
HazelcastSession session = this.repository.findById(saved.getId());
assertThat(session.getId()).isEqualTo(saved.getId());
assertThat(session.<String>getAttribute("savedName")).isEqualTo("savedValue");
verify(this.sessions, times(1)).get(eq(saved.getId()));
verifyNoMoreInteractions(this.sessions);
}
@Test
void delete() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
String sessionId = "testSessionId";
this.repository.deleteById(sessionId);
verify(this.sessions, times(1)).remove(eq(sessionId));
verifyNoMoreInteractions(this.sessions);
}
@Test
void findByIndexNameAndIndexValueUnknownIndexName() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
String indexValue = "testIndexValue";
Map<String, HazelcastSession> sessions = this.repository.findByIndexNameAndIndexValue("testIndexName",
indexValue);
assertThat(sessions).isEmpty();
verifyNoMoreInteractions(this.sessions);
}
@Test
void findByIndexNameAndIndexValuePrincipalIndexNameNotFound() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
String principal = "username";
Map<String, HazelcastSession> sessions = this.repository
.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principal);
assertThat(sessions).isEmpty();
verify(this.sessions, times(1)).values(isA(EqualPredicate.class));
verifyNoMoreInteractions(this.sessions);
}
@Test
void findByIndexNameAndIndexValuePrincipalIndexNameFound() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
String principal = "username";
Authentication authentication = new UsernamePasswordAuthenticationToken(principal, "notused",
AuthorityUtils.createAuthorityList("ROLE_USER"));
List<MapSession> saved = new ArrayList<>(2);
MapSession saved1 = new MapSession();
saved1.setAttribute(SPRING_SECURITY_CONTEXT, authentication);
saved.add(saved1);
MapSession saved2 = new MapSession();
saved2.setAttribute(SPRING_SECURITY_CONTEXT, authentication);
saved.add(saved2);
given(this.sessions.values(isA(EqualPredicate.class))).willReturn(saved);
Map<String, HazelcastSession> sessions = this.repository
.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principal);
assertThat(sessions).hasSize(2);
verify(this.sessions, times(1)).values(isA(EqualPredicate.class));
verifyNoMoreInteractions(this.sessions);
}
@Test // gh-1120
void getAttributeNamesAndRemove() {
HazelcastSession session = this.repository.createSession();
session.setAttribute("attribute1", "value1");
session.setAttribute("attribute2", "value2");
for (String attributeName : session.getAttributeNames()) {
session.removeAttribute(attributeName);
}
assertThat(session.getAttributeNames()).isEmpty();
}
@Test
@SuppressWarnings("unchecked")
void saveWithSaveModeOnSetAttribute() {
verify(this.sessions).addEntryListener(any(MapListener.class), anyBoolean());
this.repository.setSaveMode(SaveMode.ON_SET_ATTRIBUTE);
MapSession delegate = new MapSession();
delegate.setAttribute("attribute1", "value1");
delegate.setAttribute("attribute2", "value2");
delegate.setAttribute("attribute3", "value3");
HazelcastSession session = this.repository.new HazelcastSession(delegate, false);
session.getAttribute("attribute2");
session.setAttribute("attribute3", "value4");
this.repository.save(session);
ArgumentCaptor<Hazelcast4SessionUpdateEntryProcessor> captor = ArgumentCaptor
.forClass(Hazelcast4SessionUpdateEntryProcessor.class);
verify(this.sessions).executeOnKey(eq(session.getId()), captor.capture());
assertThat((Map<String, Object>) ReflectionTestUtils.getField(captor.getValue(), "delta")).hasSize(1);
verifyNoMoreInteractions(this.sessions);
}
@Test
@SuppressWarnings("unchecked")
void saveWithSaveModeOnGetAttribute() {
verify(this.sessions).addEntryListener(any(MapListener.class), anyBoolean());
this.repository.setSaveMode(SaveMode.ON_GET_ATTRIBUTE);
MapSession delegate = new MapSession();
delegate.setAttribute("attribute1", "value1");
delegate.setAttribute("attribute2", "value2");
delegate.setAttribute("attribute3", "value3");
HazelcastSession session = this.repository.new HazelcastSession(delegate, false);
session.getAttribute("attribute2");
session.setAttribute("attribute3", "value4");
this.repository.save(session);
ArgumentCaptor<Hazelcast4SessionUpdateEntryProcessor> captor = ArgumentCaptor
.forClass(Hazelcast4SessionUpdateEntryProcessor.class);
verify(this.sessions).executeOnKey(eq(session.getId()), captor.capture());
assertThat((Map<String, Object>) ReflectionTestUtils.getField(captor.getValue(), "delta")).hasSize(2);
verifyNoMoreInteractions(this.sessions);
}
@Test
@SuppressWarnings("unchecked")
void saveWithSaveModeAlways() {
verify(this.sessions).addEntryListener(any(MapListener.class), anyBoolean());
this.repository.setSaveMode(SaveMode.ALWAYS);
MapSession delegate = new MapSession();
delegate.setAttribute("attribute1", "value1");
delegate.setAttribute("attribute2", "value2");
delegate.setAttribute("attribute3", "value3");
HazelcastSession session = this.repository.new HazelcastSession(delegate, false);
session.getAttribute("attribute2");
session.setAttribute("attribute3", "value4");
this.repository.save(session);
ArgumentCaptor<Hazelcast4SessionUpdateEntryProcessor> captor = ArgumentCaptor
.forClass(Hazelcast4SessionUpdateEntryProcessor.class);
verify(this.sessions).executeOnKey(eq(session.getId()), captor.capture());
assertThat((Map<String, Object>) ReflectionTestUtils.getField(captor.getValue(), "delta")).hasSize(3);
verifyNoMoreInteractions(this.sessions);
}
}

View File

@@ -32,13 +32,13 @@ import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
class SessionUpdateEntryProcessorTest {
class Hazelcast4SessionUpdateEntryProcessorTest {
private SessionUpdateEntryProcessor processor;
private Hazelcast4SessionUpdateEntryProcessor processor;
@BeforeEach
void setUp() {
this.processor = new SessionUpdateEntryProcessor();
this.processor = new Hazelcast4SessionUpdateEntryProcessor();
}
@Test

View File

@@ -1,11 +1,18 @@
apply plugin: 'io.spring.convention.spring-module'
configurations {
hazelcast4
}
dependencies {
api project(':spring-session-core')
api "com.hazelcast:hazelcast"
api "jakarta.annotation:jakarta.annotation-api"
api "org.springframework:spring-context"
hazelcast4(project(path: ":hazelcast4", configuration: 'classesOnlyElements'))
compileOnly(project(":hazelcast4"))
testImplementation "jakarta.servlet:jakarta.servlet-api"
testImplementation "org.assertj:assertj-core"
testImplementation "org.mockito:mockito-core"
@@ -13,7 +20,13 @@ dependencies {
testImplementation "org.springframework:spring-web"
testImplementation "org.springframework.security:spring-security-core"
testImplementation "org.junit.jupiter:junit-jupiter-api"
testRuntimeOnly project(':hazelcast4')
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine"
integrationTestCompile "com.hazelcast:hazelcast-client"
integrationTestCompile "org.testcontainers:testcontainers"
}
jar {
from configurations.hazelcast4
}

View File

@@ -16,12 +16,9 @@
package org.springframework.session.hazelcast;
import java.time.Duration;
import java.time.Instant;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.instance.impl.HazelcastInstanceProxy;
import com.hazelcast.map.IMap;
import com.hazelcast.core.IMap;
import com.hazelcast.instance.HazelcastInstanceProxy;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;
@@ -228,51 +225,4 @@ abstract class AbstractHazelcastIndexedSessionRepositoryITests {
this.repository.deleteById(session.getId());
}
@Test
void createAndUpdateSessionWhileKeepingOriginalTimeToLiveConfiguredOnRepository() {
final Duration defaultSessionTimeout = Duration.ofSeconds(1800);
final IMap<String, MapSession> hazelcastMap = this.hazelcastInstance
.getMap(HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME);
HazelcastSession session = this.repository.createSession();
String sessionId = session.getId();
this.repository.save(session);
assertThat(session.getMaxInactiveInterval()).isEqualTo(defaultSessionTimeout);
assertThat(hazelcastMap.getEntryView(sessionId).getTtl()).isEqualTo(defaultSessionTimeout.toMillis());
session = this.repository.findById(sessionId);
session.setLastAccessedTime(Instant.now());
this.repository.save(session);
session = this.repository.findById(sessionId);
assertThat(session.getMaxInactiveInterval()).isEqualTo(defaultSessionTimeout);
assertThat(hazelcastMap.getEntryView(sessionId).getTtl()).isEqualTo(defaultSessionTimeout.toMillis());
}
@Test
void createAndUpdateSessionWhileKeepingTimeToLiveSetOnSession() {
final Duration individualSessionTimeout = Duration.ofSeconds(23);
final IMap<String, MapSession> hazelcastMap = this.hazelcastInstance
.getMap(HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME);
HazelcastSession session = this.repository.createSession();
session.setMaxInactiveInterval(individualSessionTimeout);
String sessionId = session.getId();
this.repository.save(session);
assertThat(session.getMaxInactiveInterval()).isEqualTo(individualSessionTimeout);
assertThat(hazelcastMap.getEntryView(sessionId).getTtl()).isEqualTo(individualSessionTimeout.toMillis());
session = this.repository.findById(sessionId);
session.setAttribute("attribute", "value");
this.repository.save(session);
session = this.repository.findById(sessionId);
assertThat(session.getMaxInactiveInterval()).isEqualTo(individualSessionTimeout);
assertThat(hazelcastMap.getEntryView(sessionId).getTtl()).isEqualTo(individualSessionTimeout.toMillis());
}
}

View File

@@ -21,7 +21,6 @@ import com.hazelcast.client.config.ClientConfig;
import com.hazelcast.core.HazelcastInstance;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.extension.ExtendWith;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.MountableFile;
@@ -45,13 +44,11 @@ import org.springframework.test.context.web.WebAppConfiguration;
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
@Disabled("Re-enable when Hazelcast image uses JDK 17")
class ClientServerHazelcastIndexedSessionRepositoryITests extends AbstractHazelcastIndexedSessionRepositoryITests {
private static GenericContainer container = new GenericContainer<>("hazelcast/hazelcast:5.0.2")
private static GenericContainer container = new GenericContainer<>("hazelcast/hazelcast:3.12.12")
.withExposedPorts(5701).withCopyFileToContainer(MountableFile.forClasspathResource("/hazelcast-server.xml"),
"/opt/hazelcast/hazelcast.xml")
.withEnv("HAZELCAST_CONFIG", "hazelcast.xml");
"/opt/hazelcast/hazelcast.xml");
@BeforeAll
static void setUpClass() {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2021 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.
@@ -16,10 +16,9 @@
package org.springframework.session.hazelcast;
import com.hazelcast.config.AttributeConfig;
import com.hazelcast.config.Config;
import com.hazelcast.config.IndexConfig;
import com.hazelcast.config.IndexType;
import com.hazelcast.config.MapAttributeConfig;
import com.hazelcast.config.MapIndexConfig;
import com.hazelcast.config.NetworkConfig;
import com.hazelcast.config.SerializerConfig;
import com.hazelcast.core.Hazelcast;
@@ -46,12 +45,12 @@ final class HazelcastITestUtils {
NetworkConfig networkConfig = config.getNetworkConfig();
networkConfig.setPort(0);
networkConfig.getJoin().getMulticastConfig().setEnabled(false);
AttributeConfig attributeConfig = new AttributeConfig()
MapAttributeConfig attributeConfig = new MapAttributeConfig()
.setName(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE)
.setExtractorClassName(PrincipalNameExtractor.class.getName());
.setExtractor(PrincipalNameExtractor.class.getName());
config.getMapConfig(HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME)
.addAttributeConfig(attributeConfig).addIndexConfig(
new IndexConfig(IndexType.HASH, HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE));
.addMapAttributeConfig(attributeConfig).addMapIndexConfig(
new MapIndexConfig(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE, false));
SerializerConfig serializerConfig = new SerializerConfig();
serializerConfig.setImplementation(new HazelcastSessionSerializer()).setTypeClass(MapSession.class);
config.getSerializationConfig().addSerializerConfig(serializerConfig);

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<hazelcast xmlns="http://www.hazelcast.com/schema/config"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.hazelcast.com/schema/config https://www.hazelcast.com/schema/config/hazelcast-config-5.0.xsd">
xsi:schemaLocation="http://www.hazelcast.com/schema/config https://www.hazelcast.com/schema/config/hazelcast-config-3.13.xsd">
<network>
<join>

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2021 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.
@@ -23,18 +23,16 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import com.hazelcast.core.EntryEvent;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.map.IMap;
import com.hazelcast.core.IMap;
import com.hazelcast.map.listener.EntryAddedListener;
import com.hazelcast.map.listener.EntryEvictedListener;
import com.hazelcast.map.listener.EntryExpiredListener;
import com.hazelcast.map.listener.EntryRemovedListener;
import com.hazelcast.query.Predicates;
import org.apache.commons.logging.Log;
@@ -54,6 +52,7 @@ 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
@@ -81,17 +80,16 @@ import org.springframework.util.Assert;
* programmatic Hazelcast Configuration:
*
* <pre class="code">
* AttributeConfig attributeConfig = new AttributeConfig()
* MapAttributeConfig attributeConfig = new MapAttributeConfig()
* .setName(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE)
* .setExtractorClassName(rincipalNameExtractor.class.getName());
* .setExtractor(PrincipalNameExtractor.class.getName());
*
* Config config = new Config();
*
* config.getMapConfig(HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME)
* .addAttributeConfig(attributeConfig)
* .addIndexConfig(new IndexConfig(
* IndexType.HASH,
* HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE));
* .addMapAttributeConfig(attributeConfig)
* .addMapIndexConfig(new MapIndexConfig(
* HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE, false));
*
* Hazelcast.newHazelcastInstance(config);
* </pre>
@@ -110,13 +108,12 @@ import org.springframework.util.Assert;
* @author Tommy Ludwig
* @author Mark Anderson
* @author Aleksandar Stojsavljevic
* @author Eleftheria Stein
* @since 2.2.0
*/
public class HazelcastIndexedSessionRepository
implements FindByIndexNameSessionRepository<HazelcastIndexedSessionRepository.HazelcastSession>,
EntryAddedListener<String, MapSession>, EntryEvictedListener<String, MapSession>,
EntryRemovedListener<String, MapSession>, EntryExpiredListener<String, MapSession> {
EntryRemovedListener<String, MapSession> {
/**
* The default name of map used by Spring Session to store sessions.
@@ -130,6 +127,8 @@ public class HazelcastIndexedSessionRepository
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;
@@ -153,7 +152,7 @@ public class HazelcastIndexedSessionRepository
private IMap<String, MapSession> sessions;
private UUID sessionListenerId;
private String sessionListenerId;
/**
* Create a new {@link HazelcastIndexedSessionRepository} instance.
@@ -262,6 +261,9 @@ public class HazelcastIndexedSessionRepository
entryProcessor.setLastAccessedTime(session.getLastAccessedTime());
}
if (session.maxInactiveIntervalChanged) {
if (SUPPORTS_SET_TTL) {
updateTtl(session);
}
entryProcessor.setMaxInactiveInterval(session.getMaxInactiveInterval());
}
if (!session.delta.isEmpty()) {
@@ -272,6 +274,10 @@ public class HazelcastIndexedSessionRepository
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);
@@ -333,14 +339,6 @@ public class HazelcastIndexedSessionRepository
}
}
@Override
public void entryExpired(EntryEvent<String, MapSession> event) {
if (logger.isDebugEnabled()) {
logger.debug("Session expired with id: " + event.getOldValue().getId());
}
this.eventPublisher.publishEvent(new SessionExpiredEvent(this, event.getOldValue()));
}
/**
* A custom implementation of {@link Session} that uses a {@link MapSession} as the
* basis for its mapping. It keeps track if changes have been made since last save.

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