Compare commits

..

83 Commits

Author SHA1 Message Date
Rob Winch
1bc21d5187 Release 3.0.0-M2 2022-05-18 13:08:46 -05:00
Rob Winch
2effbd19ab Temporarily Disable Samples 2022-05-18 13:00:33 -05:00
Rob Winch
31275574ee Update to spring-security-bom:6.0.0-M5
Closes gh-2093
2022-05-18 12:04:25 -05:00
Rob Winch
e4201aea05 Update to spring-data-bom:2022.0.0-M3
Closes gh-9092
2022-05-18 12:04:25 -05:00
Rob Winch
3c134778d8 Update to spring-framework-bom:6.0.0-M4
Closes gh-2091
2022-05-18 12:04:25 -05:00
Rob Winch
bc51d842dc Update to jackson-bom:2.13.3
Clsoes gh-2090
2022-05-18 12:04:25 -05:00
Rob Winch
17610e7cc2 Update to reactor-bom:2020.0.19
Closes gh-2089
2022-05-18 12:04:15 -05:00
Rob Winch
3ce78f6cd0 Fix formatting 2022-05-13 17:49:29 -05:00
Rob Winch
21b0f60721 Add .sdkmanrc 2022-05-13 17:41:34 -05:00
Rob Winch
0dcdf5f147 Fix Thymeleaf Samples
Thymeleaf removed support for accessing the HttpServletRequest and HttpSession
automatically, so we need to add any properties we want to access as ModelAttributes

Closes gh-2076
2022-05-13 17:37:33 -05:00
Rob Winch
a0bf6a0e62 Restore all Samples 2022-05-13 17:36:52 -05:00
Rob Winch
d23b81f300 Fix spring-session-sample-boot-mongodb-traditional 2022-05-13 17:36:52 -05:00
Rob Winch
f33c5fe19a Fix spring-session-sample-boot-websocket 2022-05-13 17:36:52 -05:00
Rob Winch
e1c4b25671 Fix spring-session-sample-boot-hazelcast 2022-05-13 17:36:52 -05:00
Rob Winch
9bf18059d2 Fix xpring-session-sample-boot-jdbc 2022-05-13 17:36:52 -05:00
Rob Winch
342198cdfb Fix spring-session-sample-boot-redis-json 2022-05-13 17:36:52 -05:00
Rob Winch
c151a97227 Fix spring-session-sample-boot-findbyusername 2022-05-13 17:36:52 -05:00
Rob Winch
0cb6e0ebc9 Fix spring-session-sample-boot-redis-simple 2022-05-13 17:36:52 -05:00
Rob Winch
b4c3cefcf4 fix spring-session-sample-boot-redis 2022-05-13 17:36:52 -05:00
Rob Winch
2a6a9cfb78 Fix Formatting 2022-05-13 17:36:23 -05:00
Greg L. Turnquist
55f9bc9c37 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 17:34:43 -05:00
Eleftheria Stein
c9cf1eab7b Update Htmlunit test dependency 2022-05-13 13:13:32 -05:00
Eleftheria Stein
003335df73 Update LocalServerPort import to new package 2022-05-13 13:12:38 -05:00
Eleftheria Stein
c3b8634fb4 Use Java 17 in antora pipeline 2022-04-29 11:38:12 +02:00
Eleftheria Stein
28e1ab1d8d Upgrade to Gradle 7.4.2 in buildSrc
Issue gh-2073
2022-04-29 10:56:02 +02:00
Eleftheria Stein
ff8750e9c1 Re-enable working samples
The Spring Boot servlet samples remain disalbed because of gh-2076.
2022-04-29 10:33:32 +02:00
Eleftheria Stein
e51dd2d1b0 Upgrade test dependencies 2022-04-29 10:28:34 +02:00
Eleftheria Stein
a6f24bc27e Upgrade MongoDB to 4.6.0
Closes gh-2075
2022-04-29 10:28:01 +02:00
Eleftheria Stein
42580c3a44 Upgrade samples to Spring Boot 3.0.0-SNAPSHOT #
Closes gh-2074
2022-04-29 10:25:48 +02:00
Eleftheria Stein
ea0aef9d97 Upgrade to Gradle 7.4.2
Closes gh-2073
2022-04-29 10:21:39 +02:00
Eleftheria Stein
0d458a4a5b Update Redis docs
Issue gh-1711
2022-04-29 09:03:57 +02:00
Eleftheria Stein
102027a456 Add Caffeine community extension
Closes gh-2039
2022-04-27 09:53:55 +02:00
Eleftheria Stein
e580a97c0c Fix link to Infinispan cache 2022-04-27 09:53:55 +02:00
Eleftheria Stein
7227949afb Fix formatting 2022-04-27 09:53:55 +02:00
Greg L. Turnquist
34d59a0ed9 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-27 09:53:55 +02:00
Eleftheria Stein
8582b9706d Use simple Redis repository by default
Closes gh-1711
2022-04-14 13:17:28 +02:00
Jerome Prinet
14ecf21c94 Update Gradle Enterprise plugin to 3.9 2022-04-14 11:11:31 +02:00
John Blum
6fc4097c2e Switch to Spring Security BOM 6.0.0-SNAPSHOT 2022-04-11 15:41:24 -07:00
John Blum
a1cfbcae0c Switch to Spring Data BOM 2022.0.0-SNAPSHOT 2022-04-11 15:40:50 -07:00
John Blum
004cf6656b Switch to Spring Framework BOM 6.0.0-SNAPSHOT 2022-04-11 15:39:51 -07:00
Felix Scheinost
cde256e1a3 Fix bug in JDBC SaveMode.ON_GET_ATTRIBUTE
Closes gh-2040
2022-04-08 17:20:46 +02:00
Eleftheria Stein
63f7f7b0a9 Upgrade Spring Data to 2022.0.0-M3
Closes gh-2048
2022-04-01 18:21:23 +02:00
Eleftheria Stein
140cc75583 Make RedisSessionRepository.DEFAULT_KEY_NAMESPACE public
Closes gh-2043
2022-03-15 18:34:59 +01:00
Eleftheria Stein
e157700087 Update to Antora 3.0.1
Closes gh-2038
2022-03-11 15:48:26 +01:00
Eleftheria Stein
1b18d64220 Fix 2.6.2 reference docs
Closes gh-2035
2022-03-11 15:48:18 +01:00
Eleftheria Stein
14756984fd Document release process for 3.0.x
Issueh gh-2036
2022-02-24 13:02:33 +01:00
Eleftheria Stein
34199baded Update Websocket sample to be compatible with H2 2.0
Closes gh-2013
2022-01-27 14:12:23 +01:00
Ruslan Molchanov
cc5bb1f3a2 Fix memory leak with null principal in Redis 2022-01-20 09:52:44 +01:00
Eleftheria Stein
48cf6849fe Next development version 2022-01-18 12:30:47 +01:00
Eleftheria Stein
0940451d50 Release 3.0.0-M1 2022-01-18 11:34:31 +01:00
Eleftheria Stein
304a6762b6 Update license acceptance for test container 2022-01-18 10:11:54 +01:00
Eleftheria Stein
b6417a9c7b Upgrade Spring Security to 6.0.0-M1
Closes gh-2010
2022-01-17 17:20:38 +01:00
Jerome Prinet
02885dca6d Bump up Gradle plugin dependencies 2022-01-17 17:20:02 +01:00
Eleftheria Stein
88aa71b3f3 Upgrade test dependencies 2022-01-17 14:17:37 +01:00
Eleftheria Stein
499cb75b1b Upgrade Hazelcast to 5.0.2
Closes gh-2011
2022-01-17 14:06:55 +01:00
Eleftheria Stein
104bcefbc1 Upgrade Reactor to 2020.0.15
Closes gh-2009
2022-01-17 13:56:33 +01:00
Eleftheria Stein
029d2cf3de Upgrade MongoDB to 4.4.1
Closes gh-2004
2022-01-17 09:38:52 +01:00
Eleftheria Stein
108e108e47 Upgrade Jackson to 2.13.1
Closes gh-2003
2022-01-17 09:38:30 +01:00
Eleftheria Stein
1bc3bd2e6f Upgrade Spring Framework to 6.0.0-M2
Closes gh-2005
2022-01-14 17:18:12 +01:00
Eleftheria Stein
24a3203755 Upgrade Spring Data to 2022.0.0-M1
Closes gh-2006
2022-01-14 17:11:16 +01:00
Jerome Prinet
77230a3318 Bump up Gradle enterprise plugin to 3.7.2 2022-01-14 16:30:49 +01:00
Eleftheria Stein
b5cdb193a8 GitHub Actions uses spring-builds+github user
This is more clear than spring-builds user
2022-01-14 16:28:23 +01:00
Eleftheria Stein
88e213d6a9 Update README to include MongoDB
Issue gh-1901
2022-01-14 16:28:16 +01:00
Eleftheria Stein
ec9db3fb5e Upgrade MongoDB to 4.4.0
Closes gh-1967
2021-11-19 17:25:14 +01:00
Guillaume Husta
91f20ca58c Doc : typo on Username _user-
user should be in italic
2021-11-19 15:14:58 +01:00
Eleftheria Stein
dc7a52b350 Update to Gradle 7.3
Closes gh-1959
2021-11-19 15:13:41 +01:00
Eleftheria Stein
979598e3c3 Re-enable Hazelcast support
Closes gh-1958
2021-11-12 16:43:30 +01:00
Eleftheria Stein
2113c32ed0 Upgrade to Hazelcast 5
Closes gh-1957
2021-11-12 16:43:02 +01:00
Eleftheria Stein
73149d2130 Enable CI 2021-11-12 12:16:21 +01:00
Eleftheria Stein
20e1fa10d9 Use JDK 11 in buildSrc 2021-11-12 12:05:36 +01:00
Eleftheria Stein
43e3c27169 Use Java 17 in deploy reference workflow
Issue gh-1945
2021-11-12 11:44:31 +01:00
Eleftheria Stein
f93d0be10e Correct version in antora.yml 2021-11-12 11:36:02 +01:00
Eleftheria Stein
d5ebb14f8d Temporarily remove unsupported samples
Closes gh-1955
2021-11-12 11:18:00 +01:00
Eleftheria Stein
00ba1b2028 Temporarily remove Hazelcast support
Closes gh-1954
2021-11-12 11:14:47 +01:00
Eleftheria Stein
cc1fd826ac Remove unnecessary code from samples
Remove code that is not necessary to demonstrate Spring Session functionality and is not compatible with the Java 17 / Jakarta EE 9 upgrades.

Issue gh-1949
2021-11-12 10:42:23 +01:00
Eleftheria Stein
fc5f875036 Upgrade Javaconfig samples to Gretty 4
Closes gh-1953
2021-11-12 10:42:14 +01:00
Eleftheria Stein
8e8cc7b1f0 Upgrade to Spring Security 6
Closes gh-1952
2021-11-12 10:34:27 +01:00
Eleftheria Stein
c2ed6a31be Upgrade to Spring Data 2022.1
Closes gh-1951
2021-11-12 10:34:27 +01:00
Eleftheria Stein
a9b9ae347f Upgrade to Spring Framework 6.0
Closes gh-1950
2021-11-12 10:34:27 +01:00
Eleftheria Stein
f697850d23 Remove Hazelcast 3 support
Closes gh-1947
2021-11-12 10:34:11 +01:00
Eleftheria Stein
182d24219c Migrate to Jakarta EE 9
Closes gh-1949
2021-11-11 18:21:26 +01:00
Eleftheria Stein
712e7d5a41 Upgrade to Java 17 baseline
Closes gh-1945
2021-11-11 18:05:15 +01:00
Eleftheria Stein
09ea3e4d39 Update to Spring Session 3.0 2021-11-11 18:02:38 +01:00
201 changed files with 1919 additions and 3651 deletions

View File

@@ -1,7 +1,6 @@
name: Generate Antora Files and Request Build
on:
workflow_dispatch: # Manual trigger
push:
branches-ignore:
- 'gh-pages'
@@ -15,6 +14,12 @@ 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: [8, 11]
jdk: [17]
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: '8'
java-version: '17'
- 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: '8'
java-version: '17'
- 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 11
- name: Set up JDK 17
uses: actions/setup-java@v2
with:
java-version: '11'
java-version: '17'
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: [8, 11]
jdk: [17]
fail-fast: false
steps:
- uses: actions/checkout@v2

View File

@@ -3,4 +3,4 @@
# See https://sdkman.io/usage#config
# A summary is to add the following to ~/.sdkman/etc/config
# sdkman_auto_env=true
java=8.0.332-zulu
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 `2.7.0-M1`, `2.7.0-RC1`, `2.7.3`
Update the version number in `gradle.properties` for the release, for example `3.0.0-M1`, `3.0.0-RC1`, `3.0.4`
== 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: '2.7.0-RC1'
version: '3.0.0-RC1'
prerelease: 'true'
display_version: '2.7.0-RC1'
display_version: '3.0.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 2.7.0-RC1
git push origin 2.7.0-RC1
git tag 3.0.0-RC1
git push origin 3.0.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 `+2.7.1+` or `+2.7.0-M1+`. +
Note 1: `+$MILESTONE+` is something like `+3.0.4+` or `+3.0.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 = '2.7.0-M3'
springBootVersion = '3.0.0-SNAPSHOT'
}
repositories {
@@ -35,7 +35,7 @@ subprojects {
apply plugin: 'io.spring.javaformat'
plugins.withType(JavaPlugin) {
sourceCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_17
}
tasks.withType(Test) {

View File

@@ -4,7 +4,7 @@ plugins {
id "groovy"
}
sourceCompatibility = 1.8
sourceCompatibility = JavaVersion.VERSION_11
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.55.0'
implementation 'net.sourceforge.htmlunit:htmlunit:2.37.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.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-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.2'
toolVersion = '0.8.7'
}
}
}

View File

@@ -36,7 +36,7 @@ public class SpringSampleWarPlugin extends SpringSamplePlugin {
pluginManager.apply("org.gretty");
project.gretty {
servletContainer = 'tomcat85'
servletContainer = 'tomcat10'
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.html");
File index = new File(testKit.getRootDir(), "build/api/allclasses-index.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:4.0.4'
optional 'jakarta.servlet:jakarta.servlet-api:5.0.0'
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 javax.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequest;
public class TheTest {
@Test

View File

@@ -1,10 +1,10 @@
plugins {
id "org.gretty" version "3.0.7"
id "org.gretty" version "4.0.0"
id "io.spring.convention.spring-sample-war"
}
dependencies {
provided 'jakarta.servlet:jakarta.servlet-api:4.0.4'
provided 'jakarta.servlet:jakarta.servlet-api:5.0.0'
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 javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.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=2.7.0
version=3.0.0-M2

View File

@@ -1,49 +1,45 @@
dependencyManagement {
imports {
mavenBom 'io.projectreactor:reactor-bom:2020.0.19'
mavenBom 'com.fasterxml.jackson:jackson-bom:2.13.2.20220328'
mavenBom 'com.fasterxml.jackson:jackson-bom:2.13.3'
mavenBom 'org.junit:junit-bom:5.8.2'
mavenBom 'org.springframework:spring-framework-bom:5.3.20'
mavenBom 'org.springframework.data:spring-data-bom:2021.2.0'
mavenBom 'org.springframework.security:spring-security-bom:5.7.0'
mavenBom 'org.testcontainers:testcontainers-bom:1.17.1'
mavenBom 'org.springframework:spring-framework-bom:6.0.0-M4'
mavenBom 'org.springframework.data:spring-data-bom:2022.0.0-M3'
mavenBom 'org.springframework.security:spring-security-bom:6.0.0-M5'
mavenBom 'org.testcontainers:testcontainers-bom:1.16.2'
}
dependencies {
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.hazelcast:hazelcast:5.0.2'
dependency 'org.aspectj:aspectjweaver:1.9.7'
dependency 'ch.qos.logback:logback-core:1.2.10'
dependency 'com.google.code.findbugs:jsr305:3.0.2'
dependency 'com.h2database:h2:1.4.200'
dependency 'com.ibm.db2:jcc:11.5.7.0'
dependency 'com.h2database:h2:2.1.212'
dependency 'com.ibm.db2:jcc:11.5.6.0'
dependency 'com.microsoft.sqlserver:mssql-jdbc:9.4.1.jre8'
dependency 'com.oracle.database.jdbc:ojdbc8:21.5.0.0'
dependency 'com.oracle.database.jdbc:ojdbc8:21.4.0.0.1'
dependency 'com.zaxxer:HikariCP:3.4.5'
dependency 'edu.umd.cs.mtc:multithreadedtc:1.01'
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 '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 '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.5.2'
dependency 'org.mariadb.jdbc:mariadb-java-client:2.7.5'
dependencySet(group: 'org.mockito', version: '4.4.0') {
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') {
entry 'mockito-core'
entry 'mockito-junit-jupiter'
}
dependencySet(group: 'org.mongodb', version: '4.5.1') {
dependencySet(group: 'org.mongodb', version: '4.6.0') {
entry 'mongodb-driver-core'
entry 'mongodb-driver-sync'
entry 'mongodb-driver-reactivestreams'
}
dependency 'org.postgresql:postgresql:42.3.4'
dependency 'org.postgresql:postgresql:42.3.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.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -18,13 +18,11 @@ 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
project(":$dir.name").projectDir = dir
}
//file('spring-session-samples').eachDirMatch(~/spring-session-sample-.*/) { dir ->
// include dir.name
// project(":$dir.name").projectDir = dir
//}
rootProject.children.each { project ->
project.buildFileName = "${project.name}.gradle"

View File

@@ -19,10 +19,10 @@ package org.springframework.session.config.annotation.web.http;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.servlet.ServletContext;
import javax.servlet.SessionCookieConfig;
import javax.servlet.http.HttpSessionListener;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.ServletContext;
import jakarta.servlet.SessionCookieConfig;
import jakarta.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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 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.
@@ -19,10 +19,10 @@ package org.springframework.session.web.context;
import java.util.Arrays;
import java.util.EnumSet;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterRegistration.Dynamic;
import javax.servlet.ServletContext;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterRegistration.Dynamic;
import jakarta.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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.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 javax.servlet.http.HttpServletRequest#isSecure()} returns true, the cookie will
* be marked as secure. For example:
* {@link jakarta.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 javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.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 javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -88,7 +88,8 @@ public class DefaultCookieSerializer implements CookieSerializer {
private String sameSite = "Lax";
/*
* @see org.springframework.session.web.http.CookieSerializer#readCookieValues(javax.
* @see
* org.springframework.session.web.http.CookieSerializer#readCookieValues(jakarta.
* 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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.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 javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionBindingListener;
import javax.servlet.http.HttpSessionContext;
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 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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.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 javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.WriteListener;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponseWrapper;
/**
* Base class for response wrappers which encapsulate the logic for handling an event when
* the {@link javax.servlet.http.HttpServletResponse} is committed.
* the {@link jakarta.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 javax.servlet.http.HttpServletResponse} is committed. This can be useful in
* the event that Async Web Requests are made.
* {@link jakarta.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 javax.servlet.http.HttpServletResponse}
* being committed.
* Implement the logic for handling the
* {@link jakarta.servlet.http.HttpServletResponse} being committed.
*/
protected abstract void onResponseCommitted();
@@ -474,8 +474,9 @@ 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 javax.servlet.ServletOutputStream} to ensure that the behavior is as close
* to the original {@link javax.servlet.ServletOutputStream} as possible. See SEC-2039
* {@link jakarta.servlet.ServletOutputStream} to ensure that the behavior is as close
* to the original {@link jakarta.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 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;
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;
/**
* 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 javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import jakarta.servlet.ServletContext;
import jakarta.servlet.http.HttpSession;
import jakarta.servlet.http.HttpSessionEvent;
import jakarta.servlet.http.HttpSessionListener;
import org.springframework.context.ApplicationListener;
import org.springframework.session.Session;
@@ -79,9 +79,8 @@ public class SessionEventHttpSessionListenerAdapter
}
/*
* @see
* org.springframework.web.context.ServletContextAware#setServletContext(javax.servlet
* .ServletContext)
* @see org.springframework.web.context.ServletContextAware#setServletContext(jakarta.
* 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 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 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 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 javax.servlet.http.HttpSession} implementation to be backed by a
* Switches the {@link jakarta.servlet.http.HttpSession} implementation to be backed by a
* {@link org.springframework.session.Session}.
*
* The {@link SessionRepositoryFilter} wraps the
* {@link javax.servlet.http.HttpServletRequest} and overrides the methods to get an
* {@link javax.servlet.http.HttpSession} to be backed by a
* {@link jakarta.servlet.http.HttpServletRequest} and overrides the methods to get an
* {@link jakarta.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 javax.servlet.http.HttpSession} and the
* {@link jakarta.servlet.http.HttpSession} and the
* {@link org.springframework.session.Session} abstraction. Specifically:
*
* <ul>
* <li>The session id is looked up using
* {@link HttpSessionIdResolver#resolveSessionIds(javax.servlet.http.HttpServletRequest)}
* {@link HttpSessionIdResolver#resolveSessionIds(jakarta.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(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, String)}
* {@link HttpSessionIdResolver#setSessionId(jakarta.servlet.http.HttpServletRequest, jakarta.servlet.http.HttpServletResponse, String)}
* <li>The client is notified that the session id is no longer valid with
* {@link HttpSessionIdResolver#expireSession(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)}
* {@link HttpSessionIdResolver#expireSession(jakarta.servlet.http.HttpServletRequest, jakarta.servlet.http.HttpServletResponse)}
* </li>
* </ul>
*
@@ -183,8 +183,8 @@ public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFi
}
/**
* A {@link javax.servlet.http.HttpServletRequest} that retrieves the
* {@link javax.servlet.http.HttpSession} using a
* A {@link jakarta.servlet.http.HttpServletRequest} that retrieves the
* {@link jakarta.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 javax.servlet.http.HttpSession;
import jakarta.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 javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.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 javax.servlet.ServletContext;
import jakarta.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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.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 javax.servlet.http.Cookie;
import jakarta.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 javax.servlet.http.Cookie;
import jakarta.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 javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 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.
@@ -20,12 +20,12 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
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 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 org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 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.
@@ -19,9 +19,9 @@ package org.springframework.session.web.http;
import java.util.Arrays;
import java.util.Collections;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import jakarta.servlet.ServletContext;
import jakarta.servlet.http.HttpSessionEvent;
import jakarta.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 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 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 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 javax.servlet.http.HttpSession;
import jakarta.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.4.1";
private static final String DOCKER_IMAGE = "mongo:4.0.10";
@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.4.1";
private static final String DOCKER_IMAGE = "mongo:4.0.10";
@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.4.1";
private static final String DOCKER_IMAGE = "mongo:4.0.10";
@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.14";
private static final String DOCKER_IMAGE = "redis:5.0.10";
protected static class BaseConfig {

View File

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* 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.
@@ -26,13 +26,10 @@ 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;
@@ -223,17 +220,9 @@ class RedisSessionRepositoryITests extends AbstractRedisITests {
}
@Configuration
@EnableSpringHttpSession
@EnableRedisHttpSession
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-2019 the original author or authors.
* 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.
@@ -113,7 +113,7 @@ class EnableRedisHttpSessionExpireSessionDestroyedTests<S extends Session> exten
}
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1)
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1, enableIndexingAndEvents = true)
static class Config extends BaseConfig {
@Bean

View File

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* 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.
@@ -33,6 +33,7 @@ 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;
/**
@@ -54,7 +55,8 @@ import org.springframework.session.web.http.SessionRepositoryFilter;
* }
* </pre>
*
* More advanced configurations can extend {@link RedisHttpSessionConfiguration} instead.
* More advanced configurations can extend {@link RedisHttpSessionConfiguration} or
* {@link RedisIndexedHttpSessionConfiguration} instead.
*
* @author Rob Winch
* @author Vedran Pavic
@@ -64,7 +66,7 @@ import org.springframework.session.web.http.SessionRepositoryFilter;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(RedisHttpSessionConfiguration.class)
@Import(RedisHttpSessionConfigurationSelector.class)
@Configuration(proxyBeanMethods = false)
public @interface EnableRedisHttpSession {
@@ -113,13 +115,6 @@ 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.
@@ -128,4 +123,13 @@ 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-2019 the original author or authors.
* 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.
@@ -16,55 +16,35 @@
package org.springframework.session.data.redis.config.annotation.web.http;
import java.util.Arrays;
import java.util.Collections;
import java.time.Duration;
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.RedisSessionRepository;
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;
@@ -83,86 +63,39 @@ 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 = RedisIndexedSessionRepository.DEFAULT_NAMESPACE;
private String redisNamespace = RedisSessionRepository.DEFAULT_KEY_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 List<SessionRepositoryCustomizer<RedisSessionRepository>> 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);
public RedisSessionRepository sessionRepository() {
RedisTemplate<String, Object> redisTemplate = createRedisTemplate();
RedisSessionRepository sessionRepository = new RedisSessionRepository(redisTemplate);
sessionRepository.setDefaultMaxInactiveInterval(Duration.ofSeconds(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;
}
@@ -186,20 +119,6 @@ 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,
@@ -217,31 +136,9 @@ 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<RedisIndexedSessionRepository>> sessionRepositoryCustomizers) {
ObjectProvider<SessionRepositoryCustomizer<RedisSessionRepository>> sessionRepositoryCustomizers) {
this.sessionRepositoryCustomizers = sessionRepositoryCustomizers.orderedStream().collect(Collectors.toList());
}
@@ -273,14 +170,10 @@ 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<Object, Object> createRedisTemplate() {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
private RedisTemplate<String, Object> createRedisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
if (this.defaultRedisSerializer != null) {
@@ -292,77 +185,4 @@ 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

@@ -0,0 +1,46 @@
/*
* 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

@@ -0,0 +1,361 @@
/*
* 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-2019 the original author or authors.
* 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.
@@ -49,14 +49,14 @@ class EnableRedisKeyspaceNotificationsInitializerTests {
@Captor
ArgumentCaptor<String> options;
private RedisHttpSessionConfiguration.EnableRedisKeyspaceNotificationsInitializer initializer;
private RedisIndexedHttpSessionConfiguration.EnableRedisKeyspaceNotificationsInitializer initializer;
@BeforeEach
void setup() {
MockitoAnnotations.initMocks(this);
given(this.connectionFactory.getConnection()).willReturn(this.connection);
this.initializer = new RedisHttpSessionConfiguration.EnableRedisKeyspaceNotificationsInitializer(
this.initializer = new RedisIndexedHttpSessionConfiguration.EnableRedisKeyspaceNotificationsInitializer(
this.connectionFactory, new ConfigureNotifyKeyspaceEventsAction());
}

View File

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* 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.
@@ -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.RedisHttpSessionConfiguration.EnableRedisKeyspaceNotificationsInitializer;
import org.springframework.session.data.redis.config.annotation.web.http.RedisIndexedHttpSessionConfiguration.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 RedisHttpSessionConfigurationMockTests {
class RedisIndexedHttpSessionConfigurationMockTests {
@Mock
RedisConnectionFactory factory;

View File

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

View File

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

View File

@@ -0,0 +1,471 @@
/*
* 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,8 +9,7 @@ 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,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']
branches: [main]
start_path: spring-session-docs
urls:
latest_version_segment_strategy: redirect:to

View File

@@ -1,2 +1,3 @@
name: ROOT
version: '2.7.0'
version: '3.0.0-M2'
prerelease: 'true'

View File

@@ -91,7 +91,6 @@ 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 = ''
@@ -121,11 +120,8 @@ function computePub (src, out, family, version, htmlUrlExtensionStyle) {
const pub = {}
let url
if (family === 'nav') {
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_)
const urlSegments = version ? [src.component, version] : [src.component]
if (src.module && src.module !== 'ROOT') urlSegments.push(src.module)
// an artificial URL used for resolving page references in navigation model
url = '/' + urlSegments.join('/') + '/'
pub.moduleRootPath = '.'

View File

@@ -19,9 +19,6 @@ 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;
@@ -38,7 +35,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.hazelcast.HazelcastIndexedSessionRepository;
import org.springframework.session.data.redis.RedisSessionRepository;
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.transaction.support.TransactionTemplate;
@@ -117,6 +114,18 @@ 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() {
@@ -171,20 +180,21 @@ 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-2019 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.
@@ -16,40 +16,44 @@
package docs.http;
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;
// 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 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>
}
}
// 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>
// }
//
// }
// end::config[]

View File

@@ -19,7 +19,7 @@ package docs.security;
import java.time.Duration;
import java.util.Base64;
import javax.servlet.http.Cookie;
import jakarta.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 javax.servlet.http.Cookie;
import jakarta.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.RedisHttpSessionConfiguration"/>
<bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisIndexedHttpSessionConfiguration"/>
<bean class="docs.http.AbstractHttpSessionListenerTests"
factory-method="createMockRedisConnection"/>

View File

@@ -148,6 +148,78 @@ 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`
@@ -170,12 +242,13 @@ 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`
=== Using `@EnableRedisHttpSession(enableIndexingAndEvents = true)`
In a web environment, the simplest way to create a new `RedisIndexedSessionRepository` is to use `@EnableRedisHttpSession`.
In a web environment, the simplest way to create a new `RedisIndexedSessionRepository` is to use `@EnableRedisHttpSession(enableIndexingAndEvents = true)`.
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`.
@@ -335,7 +408,7 @@ redis-cli config set notify-keyspace-events Egx
----
====
If you use `@EnableRedisHttpSession`, managing the `SessionMessageListener` and enabling the necessary Redis Keyspace events is done automatically.
If you use `@EnableRedisHttpSession(enableIndexingAndEvents = true)`, 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 configuration support documented in <<httpsession-redis,`HttpSession` with Redis>>, all you need to do is register every `HttpSessionListener` as a bean.
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.
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 8+.
* Java 17+.
* If you run in a Servlet Container (not required), Servlet 3.1+.
* If you use other Spring libraries (not required), the minimum required version is Spring 5.0.x.
* If you use other Spring libraries (not required), the minimum required version is Spring 6.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,46 +1,20 @@
[[upgrading-2.0]]
= Upgrading to 2.x
[[upgrading-3.0]]
= Upgrading to 3.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 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.
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.
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,6 +22,7 @@ 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 javax.servlet.http.Cookie;
import jakarta.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 javax.servlet.http.Cookie;
import jakarta.servlet.http.Cookie;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

View File

@@ -1,41 +0,0 @@
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

@@ -1,251 +0,0 @@
/*
* 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

@@ -1,78 +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.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

@@ -1,51 +0,0 @@
/*
* 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

@@ -1,90 +0,0 @@
/*
* 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

@@ -1,61 +0,0 @@
/*
* 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

@@ -1,241 +0,0 @@
/*
* 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

@@ -1,72 +0,0 @@
/*
* 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

@@ -1,17 +0,0 @@
<?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

@@ -1,478 +0,0 @@
/*
* 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

@@ -1,43 +0,0 @@
/*
* 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

@@ -1,83 +0,0 @@
/*
* 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

@@ -1,470 +0,0 @@
/*
* 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

@@ -1,18 +1,11 @@
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"
@@ -20,13 +13,7 @@ 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,9 +16,12 @@
package org.springframework.session.hazelcast;
import java.time.Duration;
import java.time.Instant;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.IMap;
import com.hazelcast.instance.HazelcastInstanceProxy;
import com.hazelcast.instance.impl.HazelcastInstanceProxy;
import com.hazelcast.map.IMap;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;
@@ -225,4 +228,51 @@ 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,6 +21,7 @@ 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;
@@ -44,11 +45,13 @@ 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:3.12.12")
private static GenericContainer container = new GenericContainer<>("hazelcast/hazelcast:5.0.2")
.withExposedPorts(5701).withCopyFileToContainer(MountableFile.forClasspathResource("/hazelcast-server.xml"),
"/opt/hazelcast/hazelcast.xml");
"/opt/hazelcast/hazelcast.xml")
.withEnv("HAZELCAST_CONFIG", "hazelcast.xml");
@BeforeAll
static void setUpClass() {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 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.
@@ -16,9 +16,10 @@
package org.springframework.session.hazelcast;
import com.hazelcast.config.AttributeConfig;
import com.hazelcast.config.Config;
import com.hazelcast.config.MapAttributeConfig;
import com.hazelcast.config.MapIndexConfig;
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;
@@ -45,12 +46,12 @@ final class HazelcastITestUtils {
NetworkConfig networkConfig = config.getNetworkConfig();
networkConfig.setPort(0);
networkConfig.getJoin().getMulticastConfig().setEnabled(false);
MapAttributeConfig attributeConfig = new MapAttributeConfig()
AttributeConfig attributeConfig = new AttributeConfig()
.setName(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE)
.setExtractor(PrincipalNameExtractor.class.getName());
.setExtractorClassName(PrincipalNameExtractor.class.getName());
config.getMapConfig(HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME)
.addMapAttributeConfig(attributeConfig).addMapIndexConfig(
new MapIndexConfig(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE, false));
.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);

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-3.13.xsd">
xsi:schemaLocation="http://www.hazelcast.com/schema/config https://www.hazelcast.com/schema/config/hazelcast-config-5.0.xsd">
<network>
<join>

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 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.
@@ -23,16 +23,18 @@ 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 jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import com.hazelcast.core.EntryEvent;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.IMap;
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;
@@ -52,7 +54,6 @@ 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
@@ -80,16 +81,17 @@ import org.springframework.util.ClassUtils;
* programmatic Hazelcast Configuration:
*
* <pre class="code">
* MapAttributeConfig attributeConfig = new MapAttributeConfig()
* AttributeConfig attributeConfig = new AttributeConfig()
* .setName(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE)
* .setExtractor(PrincipalNameExtractor.class.getName());
* .setExtractorClassName(rincipalNameExtractor.class.getName());
*
* Config config = new Config();
*
* config.getMapConfig(HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME)
* .addMapAttributeConfig(attributeConfig)
* .addMapIndexConfig(new MapIndexConfig(
* HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE, false));
* .addAttributeConfig(attributeConfig)
* .addIndexConfig(new IndexConfig(
* IndexType.HASH,
* HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE));
*
* Hazelcast.newHazelcastInstance(config);
* </pre>
@@ -108,12 +110,13 @@ import org.springframework.util.ClassUtils;
* @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> {
EntryRemovedListener<String, MapSession>, EntryExpiredListener<String, MapSession> {
/**
* The default name of map used by Spring Session to store sessions.
@@ -127,8 +130,6 @@ 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;
@@ -152,7 +153,7 @@ public class HazelcastIndexedSessionRepository
private IMap<String, MapSession> sessions;
private String sessionListenerId;
private UUID sessionListenerId;
/**
* Create a new {@link HazelcastIndexedSessionRepository} instance.
@@ -261,9 +262,6 @@ public class HazelcastIndexedSessionRepository
entryProcessor.setLastAccessedTime(session.getLastAccessedTime());
}
if (session.maxInactiveIntervalChanged) {
if (SUPPORTS_SET_TTL) {
updateTtl(session);
}
entryProcessor.setMaxInactiveInterval(session.getMaxInactiveInterval());
}
if (!session.delta.isEmpty()) {
@@ -274,10 +272,6 @@ 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);
@@ -339,6 +333,14 @@ 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.

View File

@@ -29,7 +29,7 @@ import org.springframework.session.MapSession;
/**
* A {@link com.hazelcast.nio.serialization.Serializer} implementation that handles the
* (de)serialization of {@link MapSession} stored on {@link com.hazelcast.core.IMap}.
* (de)serialization of {@link MapSession} stored on {@link com.hazelcast.map.IMap}.
*
* <p>
* The use of this serializer is optional and provides faster serialization of sessions.

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