API Documentation
在线浏览完整的 {docs-url}/spring-session/docs/{spring-session-version}/api/[Javadoc]。在以下各节中描述了关键的 API:
You can browse the complete {docs-url}/spring-session/docs/{spring-session-version}/api/[Javadoc] online. The key APIs are described in the following sections:
Using Session
Session
是名称值对的一个简化 Map
。
A Session
is a simplified Map
of name value pairs.
典型用法可能如下面的清单所示:
Typical usage might look like the following listing:
/*
* Copyright 2014-2023 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 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;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.mock.web.MockServletContext;
import org.springframework.session.MapSession;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.RedisSessionRepository;
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Rob Winch
* @author Vedran Pavic
*/
class IndexDocTests {
private static final String ATTR_USER = "user";
@Test
void repositoryDemo() {
RepositoryDemo<MapSession> demo = new RepositoryDemo<>();
demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());
demo.demo();
}
// tag::repository-demo[]
public class RepositoryDemo<S extends Session> {
private SessionRepository<S> repository; (1)
public void demo() {
S toSave = this.repository.createSession(); (2)
(3)
User rwinch = new User("rwinch");
toSave.setAttribute(ATTR_USER, rwinch);
this.repository.save(toSave); (4)
S session = this.repository.findById(toSave.getId()); (5)
(6)
User user = session.getAttribute(ATTR_USER);
assertThat(user).isEqualTo(rwinch);
}
// ... setter methods ...
}
// end::repository-demo[]
@Test
void expireRepositoryDemo() {
ExpiringRepositoryDemo<MapSession> demo = new ExpiringRepositoryDemo<>();
demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());
demo.demo();
}
// tag::expire-repository-demo[]
public class ExpiringRepositoryDemo<S extends Session> {
private SessionRepository<S> repository; (1)
public void demo() {
S toSave = this.repository.createSession(); (2)
// ...
toSave.setMaxInactiveInterval(Duration.ofSeconds(30)); (3)
this.repository.save(toSave); (4)
S session = this.repository.findById(toSave.getId()); (5)
// ...
}
// ... setter methods ...
}
// end::expire-repository-demo[]
@Test
void newRedisSessionRepository() {
// tag::new-redissessionrepository[]
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// ... configure redisTemplate ...
SessionRepository<? extends Session> repository = new RedisSessionRepository(redisTemplate);
// end::new-redissessionrepository[]
}
@Test
void newRedisIndexedSessionRepository() {
// tag::new-redisindexedsessionrepository[]
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// ... configure redisTemplate ...
SessionRepository<? extends Session> repository = new RedisIndexedSessionRepository(redisTemplate);
// end::new-redisindexedsessionrepository[]
}
@Test
void newReactiveRedisSessionRepository() {
LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory();
RedisSerializationContext<String, Object> serializationContext = RedisSerializationContext
.<String, Object>newSerializationContext(new JdkSerializationRedisSerializer())
.build();
// tag::new-reactiveredissessionrepository[]
// ... create and configure connectionFactory and serializationContext ...
ReactiveRedisTemplate<String, Object> redisTemplate = new ReactiveRedisTemplate<>(connectionFactory,
serializationContext);
ReactiveSessionRepository<? extends Session> repository = new ReactiveRedisSessionRepository(redisTemplate);
// end::new-reactiveredissessionrepository[]
}
@Test
void mapRepository() {
// tag::new-mapsessionrepository[]
SessionRepository<? extends Session> repository = new MapSessionRepository(new ConcurrentHashMap<>());
// end::new-mapsessionrepository[]
}
@Test
void newJdbcIndexedSessionRepository() {
// tag::new-jdbcindexedsessionrepository[]
JdbcTemplate jdbcTemplate = new JdbcTemplate();
// ... configure jdbcTemplate ...
TransactionTemplate transactionTemplate = new TransactionTemplate();
// ... configure transactionTemplate ...
SessionRepository<? extends Session> repository = new JdbcIndexedSessionRepository(jdbcTemplate,
transactionTemplate);
// end::new-jdbcindexedsessionrepository[]
}
@Test
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() {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(SpringHttpSessionConfig.class);
context.setServletContext(new MockServletContext());
context.refresh();
try {
context.getBean(SessionRepositoryFilter.class);
}
finally {
context.close();
}
}
private static final class User {
private User(String username) {
}
}
}
1 | We create a SessionRepository instance with a generic type, S , that extends Session . The generic type is defined in our class. |
2 | We create a new Session by using our SessionRepository and assign it to a variable of type S . |
3 | We interact with the Session . In our example, we demonstrate saving a User to the Session . |
4 | We now save the Session . This is why we needed the generic type S . The SessionRepository only allows saving Session instances that were created or retrieved by using the same SessionRepository . This allows for the SessionRepository to make implementation specific optimizations (that is, writing only attributes that have changed). |
5 | We retrieve the Session from the SessionRepository . |
6 | We obtain the persisted User from our Session without the need for explicitly casting our attribute. |
Session
API 还提供了与 Session
实例过期相关的属性。
The Session
API also provides attributes related to the Session
instance’s expiration.
典型用法可能如下面的清单所示:
Typical usage might look like the following listing:
/*
* Copyright 2014-2023 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 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;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.mock.web.MockServletContext;
import org.springframework.session.MapSession;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.RedisSessionRepository;
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Rob Winch
* @author Vedran Pavic
*/
class IndexDocTests {
private static final String ATTR_USER = "user";
@Test
void repositoryDemo() {
RepositoryDemo<MapSession> demo = new RepositoryDemo<>();
demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());
demo.demo();
}
// tag::repository-demo[]
public class RepositoryDemo<S extends Session> {
private SessionRepository<S> repository; (1)
public void demo() {
S toSave = this.repository.createSession(); (2)
(3)
User rwinch = new User("rwinch");
toSave.setAttribute(ATTR_USER, rwinch);
this.repository.save(toSave); (4)
S session = this.repository.findById(toSave.getId()); (5)
(6)
User user = session.getAttribute(ATTR_USER);
assertThat(user).isEqualTo(rwinch);
}
// ... setter methods ...
}
// end::repository-demo[]
@Test
void expireRepositoryDemo() {
ExpiringRepositoryDemo<MapSession> demo = new ExpiringRepositoryDemo<>();
demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());
demo.demo();
}
// tag::expire-repository-demo[]
public class ExpiringRepositoryDemo<S extends Session> {
private SessionRepository<S> repository; (1)
public void demo() {
S toSave = this.repository.createSession(); (2)
// ...
toSave.setMaxInactiveInterval(Duration.ofSeconds(30)); (3)
this.repository.save(toSave); (4)
S session = this.repository.findById(toSave.getId()); (5)
// ...
}
// ... setter methods ...
}
// end::expire-repository-demo[]
@Test
void newRedisSessionRepository() {
// tag::new-redissessionrepository[]
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// ... configure redisTemplate ...
SessionRepository<? extends Session> repository = new RedisSessionRepository(redisTemplate);
// end::new-redissessionrepository[]
}
@Test
void newRedisIndexedSessionRepository() {
// tag::new-redisindexedsessionrepository[]
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// ... configure redisTemplate ...
SessionRepository<? extends Session> repository = new RedisIndexedSessionRepository(redisTemplate);
// end::new-redisindexedsessionrepository[]
}
@Test
void newReactiveRedisSessionRepository() {
LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory();
RedisSerializationContext<String, Object> serializationContext = RedisSerializationContext
.<String, Object>newSerializationContext(new JdkSerializationRedisSerializer())
.build();
// tag::new-reactiveredissessionrepository[]
// ... create and configure connectionFactory and serializationContext ...
ReactiveRedisTemplate<String, Object> redisTemplate = new ReactiveRedisTemplate<>(connectionFactory,
serializationContext);
ReactiveSessionRepository<? extends Session> repository = new ReactiveRedisSessionRepository(redisTemplate);
// end::new-reactiveredissessionrepository[]
}
@Test
void mapRepository() {
// tag::new-mapsessionrepository[]
SessionRepository<? extends Session> repository = new MapSessionRepository(new ConcurrentHashMap<>());
// end::new-mapsessionrepository[]
}
@Test
void newJdbcIndexedSessionRepository() {
// tag::new-jdbcindexedsessionrepository[]
JdbcTemplate jdbcTemplate = new JdbcTemplate();
// ... configure jdbcTemplate ...
TransactionTemplate transactionTemplate = new TransactionTemplate();
// ... configure transactionTemplate ...
SessionRepository<? extends Session> repository = new JdbcIndexedSessionRepository(jdbcTemplate,
transactionTemplate);
// end::new-jdbcindexedsessionrepository[]
}
@Test
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() {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(SpringHttpSessionConfig.class);
context.setServletContext(new MockServletContext());
context.refresh();
try {
context.getBean(SessionRepositoryFilter.class);
}
finally {
context.close();
}
}
private static final class User {
private User(String username) {
}
}
}
1 | We create a SessionRepository instance with a generic type, S , that extends Session . The generic type is defined in our class. |
2 | We create a new Session by using our SessionRepository and assign it to a variable of type S . |
3 | We interact with the Session .
In our example, we demonstrate updating the amount of time the Session can be inactive before it expires. |
4 | We now save the Session .
This is why we needed the generic type, S .
The SessionRepository allows saving only Session instances that were created or retrieved using the same SessionRepository .
This allows for the SessionRepository to make implementation specific optimizations (that is, writing only attributes that have changed).
The last accessed time is automatically updated when the Session is saved. |
5 | We retrieve the Session from the SessionRepository .
If the Session were expired, the result would be null. |
Using SessionRepository
SessionRepository
负责创建、检索和持久化 Session
实例。
A SessionRepository
is in charge of creating, retrieving, and persisting Session
instances.
如果可能,您不应直接与 SessionRepository
或 Session
进行交互。相反,开发人员应更倾向于通过 HttpSession
和 WebSocket 集成间接与 SessionRepository
和 Session
进行交互。
If possible, you should not interact directly with a SessionRepository
or a Session
.
Instead, developers should prefer interacting with SessionRepository
and Session
indirectly through the HttpSession
and WebSocket integration.
Using FindByIndexNameSessionRepository
Spring Session 用于使用 Session
的最基本 API 是 SessionRepository
。此 API 故意非常简单,以便你可以轻松地提供具有基本功能的其他实现。
Spring Session’s most basic API for using a Session
is the SessionRepository
.
This API is intentionally very simple, so that you can easily provide additional implementations with basic functionality.
某些 SessionRepository
实现还可能选择实现 FindByIndexNameSessionRepository
。例如,Spring 的 Redis、JDBC 和 Hazelcast 支持库都实现了 FindByIndexNameSessionRepository
。
Some SessionRepository
implementations may also choose to implement FindByIndexNameSessionRepository
.
For example, Spring’s Redis, JDBC, and Hazelcast support libraries all implement FindByIndexNameSessionRepository
.
FindByIndexNameSessionRepository
提供了一个方法来查找具有给定索引名称和索引值的所有会话。作为所有提供的 FindByIndexNameSessionRepository
实现都支持的常见用例,你可以使用一个方便的方法来查找特定用户的会话。这通过确保名称为 FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME
的会话属性填充了用户名来完成。确保属性已填充是你的责任,因为 Spring Session 不知道正在使用的认证机制。以下清单中演示了如何使用此功能的示例:
The FindByIndexNameSessionRepository
provides a method to look up all the sessions with a given index name and index value.
As a common use case that is supported by all provided FindByIndexNameSessionRepository
implementations, you can use a convenient method to look up all the sessions for a particular user.
This is done by ensuring that the session attribute with the name of FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME
is populated with the username.
It is your responsibility to ensure that the attribute is populated, since Spring Session is not aware of the authentication mechanism being used.
An example of how to use this can be seen in the following listing:
/*
* 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 docs;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
/**
* @author Rob Winch
*
*/
@ExtendWith(MockitoExtension.class)
class FindByIndexNameSessionRepositoryTests {
@Mock
FindByIndexNameSessionRepository<Session> sessionRepository;
@Mock
Session session;
@Test
void setUsername() {
// tag::set-username[]
String username = "username";
this.session.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, username);
// end::set-username[]
}
@Test
void findByUsername() {
// tag::findby-username[]
String username = "username";
Map<String, Session> sessionIdToSession = this.sessionRepository.findByPrincipalName(username);
// end::findby-username[]
}
}
|
Some implementations of |
对会话编制索引后,你可以使用类似于以下代码找到会话:
Once the session is indexed, you can find by using code similar to the following:
/*
* 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 docs;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
/**
* @author Rob Winch
*
*/
@ExtendWith(MockitoExtension.class)
class FindByIndexNameSessionRepositoryTests {
@Mock
FindByIndexNameSessionRepository<Session> sessionRepository;
@Mock
Session session;
@Test
void setUsername() {
// tag::set-username[]
String username = "username";
this.session.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, username);
// end::set-username[]
}
@Test
void findByUsername() {
// tag::findby-username[]
String username = "username";
Map<String, Session> sessionIdToSession = this.sessionRepository.findByPrincipalName(username);
// end::findby-username[]
}
}
Using ReactiveSessionRepository
ReactiveSessionRepository
负责以非阻塞和反应式的方式创建、检索和持久化 Session
实例。
A ReactiveSessionRepository
is in charge of creating, retrieving, and persisting Session
instances in a non-blocking and reactive manner.
如果可能,您不应直接与 ReactiveSessionRepository
或 Session
进行交互。相反,您应更倾向于通过 WebSession 集成间接与 ReactiveSessionRepository
和 Session
进行交互。
If possible, you should not interact directly with a ReactiveSessionRepository
or a Session
.
Instead, you should prefer interacting with ReactiveSessionRepository
and Session
indirectly through the WebSession integration.
Using @EnableSpringHttpSession
你可以将 @EnableSpringHttpSession
注解添加到 @Configuration
类中,以将 SessionRepositoryFilter
公开为名为 springSessionRepositoryFilter
的 bean。要使用此注解,你必须提供一个 SessionRepository
bean。以下示例演示了如何操作:
You can add the @EnableSpringHttpSession
annotation to a @Configuration
class to expose the SessionRepositoryFilter
as a bean named springSessionRepositoryFilter
.
In order to use the annotation, you must provide a single SessionRepository
bean.
The following example shows how to do so:
/*
* Copyright 2014-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package docs;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
// tag::class[]
@EnableSpringHttpSession
@Configuration
public class SpringHttpSessionConfig {
@Bean
public MapSessionRepository sessionRepository() {
return new MapSessionRepository(new ConcurrentHashMap<>());
}
}
// end::class[]
请注意,为你配置的会话过期没有基础设施。这是因为诸如会话过期等事项高度依赖于实现方式。这意味着,如果你需要清理过期的会话,则负责清理过期的会话。
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 need to clean up expired sessions, you are responsible for cleaning up the expired sessions.
Using @EnableSpringWebSession
你可以将 @EnableSpringWebSession
注解添加到 @Configuration
类中,以将 WebSessionManager
公开为名为 webSessionManager
的 bean。要使用此注解,你必须提供一个 ReactiveSessionRepository
bean。以下示例演示了如何操作:
You can add the @EnableSpringWebSession
annotation to a @Configuration
class to expose the WebSessionManager
as a bean named webSessionManager
.
To use the annotation, you must provide a single ReactiveSessionRepository
bean.
The following example shows how to do so:
/*
* 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 docs;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.ReactiveMapSessionRepository;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.config.annotation.web.server.EnableSpringWebSession;
// tag::class[]
@Configuration(proxyBeanMethods = false)
@EnableSpringWebSession
public class SpringWebSessionConfig {
@Bean
public ReactiveSessionRepository reactiveSessionRepository() {
return new ReactiveMapSessionRepository(new ConcurrentHashMap<>());
}
}
// end::class[]
请注意,为你配置的会话过期没有基础设施。这是因为诸如会话过期等事项高度依赖于实现方式。这意味着,如果你需要清理过期的会话,则负责清理过期的会话。
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.
Using RedisSessionRepository
RedisSessionRepository
是一个 SessionRepository
,通过使用 Spring Data 的 RedisOperations
实现。在 Web 环境中,此实现通常与 SessionRepositoryFilter
结合使用。请注意,此实现不支持会话事件的发布。
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.
Instantiating a RedisSessionRepository
以下列表中展示了如何创建新实例的典型示例:
You can see a typical example of how to create a new instance in the following listing:
/*
* Copyright 2014-2023 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 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;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.mock.web.MockServletContext;
import org.springframework.session.MapSession;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.RedisSessionRepository;
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Rob Winch
* @author Vedran Pavic
*/
class IndexDocTests {
private static final String ATTR_USER = "user";
@Test
void repositoryDemo() {
RepositoryDemo<MapSession> demo = new RepositoryDemo<>();
demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());
demo.demo();
}
// tag::repository-demo[]
public class RepositoryDemo<S extends Session> {
private SessionRepository<S> repository; (1)
public void demo() {
S toSave = this.repository.createSession(); (2)
(3)
User rwinch = new User("rwinch");
toSave.setAttribute(ATTR_USER, rwinch);
this.repository.save(toSave); (4)
S session = this.repository.findById(toSave.getId()); (5)
(6)
User user = session.getAttribute(ATTR_USER);
assertThat(user).isEqualTo(rwinch);
}
// ... setter methods ...
}
// end::repository-demo[]
@Test
void expireRepositoryDemo() {
ExpiringRepositoryDemo<MapSession> demo = new ExpiringRepositoryDemo<>();
demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());
demo.demo();
}
// tag::expire-repository-demo[]
public class ExpiringRepositoryDemo<S extends Session> {
private SessionRepository<S> repository; (1)
public void demo() {
S toSave = this.repository.createSession(); (2)
// ...
toSave.setMaxInactiveInterval(Duration.ofSeconds(30)); (3)
this.repository.save(toSave); (4)
S session = this.repository.findById(toSave.getId()); (5)
// ...
}
// ... setter methods ...
}
// end::expire-repository-demo[]
@Test
void newRedisSessionRepository() {
// tag::new-redissessionrepository[]
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// ... configure redisTemplate ...
SessionRepository<? extends Session> repository = new RedisSessionRepository(redisTemplate);
// end::new-redissessionrepository[]
}
@Test
void newRedisIndexedSessionRepository() {
// tag::new-redisindexedsessionrepository[]
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// ... configure redisTemplate ...
SessionRepository<? extends Session> repository = new RedisIndexedSessionRepository(redisTemplate);
// end::new-redisindexedsessionrepository[]
}
@Test
void newReactiveRedisSessionRepository() {
LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory();
RedisSerializationContext<String, Object> serializationContext = RedisSerializationContext
.<String, Object>newSerializationContext(new JdkSerializationRedisSerializer())
.build();
// tag::new-reactiveredissessionrepository[]
// ... create and configure connectionFactory and serializationContext ...
ReactiveRedisTemplate<String, Object> redisTemplate = new ReactiveRedisTemplate<>(connectionFactory,
serializationContext);
ReactiveSessionRepository<? extends Session> repository = new ReactiveRedisSessionRepository(redisTemplate);
// end::new-reactiveredissessionrepository[]
}
@Test
void mapRepository() {
// tag::new-mapsessionrepository[]
SessionRepository<? extends Session> repository = new MapSessionRepository(new ConcurrentHashMap<>());
// end::new-mapsessionrepository[]
}
@Test
void newJdbcIndexedSessionRepository() {
// tag::new-jdbcindexedsessionrepository[]
JdbcTemplate jdbcTemplate = new JdbcTemplate();
// ... configure jdbcTemplate ...
TransactionTemplate transactionTemplate = new TransactionTemplate();
// ... configure transactionTemplate ...
SessionRepository<? extends Session> repository = new JdbcIndexedSessionRepository(jdbcTemplate,
transactionTemplate);
// end::new-jdbcindexedsessionrepository[]
}
@Test
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() {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(SpringHttpSessionConfig.class);
context.setServletContext(new MockServletContext());
context.refresh();
try {
context.getBean(SessionRepositoryFilter.class);
}
finally {
context.close();
}
}
private static final class User {
private User(String username) {
}
}
}
有关如何创建 RedisConnectionFactory
的更多信息,请参阅 Spring Data Redis 参考。
For additional information on how to create a RedisConnectionFactory
, see the Spring Data Redis Reference.
Using @EnableRedisHttpSession
在 Web 环境中,创建新的 RedisSessionRepository
的最简单方法是使用 @EnableRedisHttpSession
。您可以在 Samples and Guides (Start Here) 中找到完整的示例用法。您可以使用以下属性来自定义配置:
In a web environment, the simplest way to create a new RedisSessionRepository
is to use @EnableRedisHttpSession
.
You can find complete example usage in the Samples and Guides (Start Here).
You can use the following attributes to customize the configuration:
enableIndexingAndEvents* enableIndexingAndEvents:是否使用 RedisIndexedSessionRepository
而不是 RedisSessionRepository
。默认值是 false
。* maxInactiveIntervalInSeconds:会话过期之前的时间,以秒为单位。* redisNamespace:允许为会话配置特定于应用程序的命名空间。Redis 密钥和信道 ID 以 <redisNamespace>:
的前缀开头。* flushMode:允许指定何时将数据写入 Redis。默认值仅在 SessionRepository
中调用 save
时写入。FlushMode.IMMEDIATE
值会尽快写入 Redis。
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.
Viewing the Session in Redis
在 installing redis-cli 之后,你可以检查 Redis using the redis-cli 中的值。例如,可以再一个终端窗口中输入以下命令:redis-cli HGETALL spring:session:sessions:1
After installing redis-cli, you can inspect the values in Redis using the redis-cli. For example, you can enter the following command into a terminal window:
$ 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. |
您还可以通过使用 hkeys
命令来查看每个会话的属性。以下示例显示了如何执行此操作:
You can also view the attributes of each session by using the hkeys
command.
The following example shows how to do so:
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"
Using RedisIndexedSessionRepository
RedisIndexedSessionRepository
是通过使用 Spring Data 的 RedisOperations
实现的 SessionRepository
。在 Web 环境中,此通常与 SessionRepositoryFilter
结合使用。该实现通过 SessionMessageListener
支持 SessionDestroyedEvent
和 SessionCreatedEvent
。
RedisIndexedSessionRepository
is a SessionRepository
that is implemented by using Spring Data’s RedisOperations
.
In a web environment, this is typically used in combination with SessionRepositoryFilter
.
The implementation supports SessionDestroyedEvent
and SessionCreatedEvent
through SessionMessageListener
.
Instantiating a RedisIndexedSessionRepository
以下列表中展示了如何创建新实例的典型示例:
You can see a typical example of how to create a new instance in the following listing:
/*
* Copyright 2014-2023 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 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;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.mock.web.MockServletContext;
import org.springframework.session.MapSession;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.RedisSessionRepository;
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Rob Winch
* @author Vedran Pavic
*/
class IndexDocTests {
private static final String ATTR_USER = "user";
@Test
void repositoryDemo() {
RepositoryDemo<MapSession> demo = new RepositoryDemo<>();
demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());
demo.demo();
}
// tag::repository-demo[]
public class RepositoryDemo<S extends Session> {
private SessionRepository<S> repository; (1)
public void demo() {
S toSave = this.repository.createSession(); (2)
(3)
User rwinch = new User("rwinch");
toSave.setAttribute(ATTR_USER, rwinch);
this.repository.save(toSave); (4)
S session = this.repository.findById(toSave.getId()); (5)
(6)
User user = session.getAttribute(ATTR_USER);
assertThat(user).isEqualTo(rwinch);
}
// ... setter methods ...
}
// end::repository-demo[]
@Test
void expireRepositoryDemo() {
ExpiringRepositoryDemo<MapSession> demo = new ExpiringRepositoryDemo<>();
demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());
demo.demo();
}
// tag::expire-repository-demo[]
public class ExpiringRepositoryDemo<S extends Session> {
private SessionRepository<S> repository; (1)
public void demo() {
S toSave = this.repository.createSession(); (2)
// ...
toSave.setMaxInactiveInterval(Duration.ofSeconds(30)); (3)
this.repository.save(toSave); (4)
S session = this.repository.findById(toSave.getId()); (5)
// ...
}
// ... setter methods ...
}
// end::expire-repository-demo[]
@Test
void newRedisSessionRepository() {
// tag::new-redissessionrepository[]
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// ... configure redisTemplate ...
SessionRepository<? extends Session> repository = new RedisSessionRepository(redisTemplate);
// end::new-redissessionrepository[]
}
@Test
void newRedisIndexedSessionRepository() {
// tag::new-redisindexedsessionrepository[]
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// ... configure redisTemplate ...
SessionRepository<? extends Session> repository = new RedisIndexedSessionRepository(redisTemplate);
// end::new-redisindexedsessionrepository[]
}
@Test
void newReactiveRedisSessionRepository() {
LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory();
RedisSerializationContext<String, Object> serializationContext = RedisSerializationContext
.<String, Object>newSerializationContext(new JdkSerializationRedisSerializer())
.build();
// tag::new-reactiveredissessionrepository[]
// ... create and configure connectionFactory and serializationContext ...
ReactiveRedisTemplate<String, Object> redisTemplate = new ReactiveRedisTemplate<>(connectionFactory,
serializationContext);
ReactiveSessionRepository<? extends Session> repository = new ReactiveRedisSessionRepository(redisTemplate);
// end::new-reactiveredissessionrepository[]
}
@Test
void mapRepository() {
// tag::new-mapsessionrepository[]
SessionRepository<? extends Session> repository = new MapSessionRepository(new ConcurrentHashMap<>());
// end::new-mapsessionrepository[]
}
@Test
void newJdbcIndexedSessionRepository() {
// tag::new-jdbcindexedsessionrepository[]
JdbcTemplate jdbcTemplate = new JdbcTemplate();
// ... configure jdbcTemplate ...
TransactionTemplate transactionTemplate = new TransactionTemplate();
// ... configure transactionTemplate ...
SessionRepository<? extends Session> repository = new JdbcIndexedSessionRepository(jdbcTemplate,
transactionTemplate);
// end::new-jdbcindexedsessionrepository[]
}
@Test
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() {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(SpringHttpSessionConfig.class);
context.setServletContext(new MockServletContext());
context.refresh();
try {
context.getBean(SessionRepositoryFilter.class);
}
finally {
context.close();
}
}
private static final class User {
private User(String username) {
}
}
}
有关如何创建 RedisConnectionFactory
的更多信息,请参阅 Spring Data Redis 参考。
For additional information on how to create a RedisConnectionFactory
, see the Spring Data Redis Reference.
Using @EnableRedisHttpSession(enableIndexingAndEvents = true)
在 Web 环境中,创建新的 RedisIndexedSessionRepository
的最简单方法是使用 @EnableRedisHttpSession(enableIndexingAndEvents = true)
。您可以在 Samples and Guides (Start Here) 中找到完整的示例用法。您可以使用以下属性来自定义配置:
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 Samples and Guides (Start Here).
You can use the following attributes to customize the configuration:
-
enableIndexingAndEvents: Whether to use a
RedisIndexedSessionRepository
instead of aRedisSessionRepository
. The default isfalse
. -
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 onSessionRepository
. A value ofFlushMode.IMMEDIATE
writes to Redis as soon as possible.
Redis TaskExecutor
RedisIndexedSessionRepository
订阅以使用 RedisMessageListenerContainer
从 Redis 接收事件。你可以创建名为 springSessionRedisTaskExecutor
的 bean、bean springSessionRedisSubscriptionExecutor
或两者来自定义那些事件被调度的方式。更多有关配置 Redis 任务执行器的详细信息,请点按此处查看 {docs-url}/spring-data-redis/docs/{spring-data-redis-version}/reference/html/#redis:pubsub:subscribe:containers[here]。
RedisIndexedSessionRepository
is subscribed to receive events from Redis by using a RedisMessageListenerContainer
.
You can customize the way those events are dispatched by creating a bean named springSessionRedisTaskExecutor
, a bean springSessionRedisSubscriptionExecutor
, or both.
You can find more details on configuring Redis task executors {docs-url}/spring-data-redis/docs/{spring-data-redis-version}/reference/html/#redis:pubsub:subscribe:containers[here].
Storage Details
以下部分概述了 Redis 如何针对每个操作进行更新。以下示例显示了创建新会话的示例:
The following sections outline how Redis is updated for each operation. The following example shows an example of creating a new session:
HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 \ maxInactiveInterval 1800 \ lastAccessedTime 1404360000000 \ sessionAttr:attrName someAttrValue \ sessionAttr:attrName2 someAttrValue2 EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100 APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe "" EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800 SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe EXPIRE spring:session:expirations1439245080000 2100
后续部分描述了详细信息。
The subsequent sections describe the details.
Saving a Session
每个会话都作为一个 Hash
存储在 Redis 中。每个会话都使用 HMSET
命令进行设置和更新。以下示例展示了每个会话是如何存储的:
Each session is stored in Redis as a Hash
.
Each session is set and updated by using the HMSET
command.
The following example shows how each session is stored:
HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 \ maxInactiveInterval 1800 \ lastAccessedTime 1404360000000 \ sessionAttr:attrName someAttrValue \ sessionAttr:attrName2 someAttrValue2
在前面的示例中,关于会话存在以下语句:
In the preceding example, the following statements are true about the session:
-
The session ID is 33fdd1b6-b496-4b33-9f7d-df96679d32fe.
-
The session was created at 1404360000000 (in milliseconds since midnight of 1/1/1970 GMT).
-
The session expires in 1800 seconds (30 minutes).
-
The session was last accessed at 1404360000000 (in milliseconds since midnight of 1/1/1970 GMT).
-
The session has two attributes. The first is
attrName
, with a value ofsomeAttrValue
. The second session attribute is namedattrName2
, with a value ofsomeAttrValue2
.
Optimized Writes
由 RedisIndexedSessionRepository
管理的 Session
实例跟踪改变的属性,只更新那些属性。这意味着如果一个属性写一次并读多次,我们只需要写一次该属性。例如,假设前一部分的 lsiting 中的 attrName2
会话属性被更新。在保存时将运行以下命令:
The Session
instances managed by RedisIndexedSessionRepository
keeps track of the properties that have changed and updates only those.
This means that, if an attribute is written once and read many times, we need to write that attribute only once.
For example, assume the attrName2
session attribute from the lsiting in the preceding section was updated.
The following command would be run upon saving:
HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe sessionAttr:attrName2 newValue
Session Expiration
一个过期时间使用 EXPIRE
命令与每个会话相关联,根据 Session.getMaxInactiveInterval()
设置。以下示例显示了一个典型的 EXPIRE
命令:
An expiration is associated with each session by using the EXPIRE
command, based upon the Session.getMaxInactiveInterval()
.
The following example shows a typical EXPIRE
command:
EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100
请注意,在会话实际过期后的五分钟设置为过期时间。这是必要的,以便在会话过期时可以访问会话的值。一个过期时间在会话本身在实际过期后的五分钟设置,以确保它被清除,但仅在我们执行任何必要处理之后。
Note that the expiration that is set to five minutes after the session actually expires. This is necessary so that the value of the session can be accessed when the session expires. An expiration is set on the session itself five minutes after it actually expires to ensure that it is cleaned up, but only after we perform any necessary processing.
|
The |
Spring Session 依赖 Redis 中的删除和已过期的 keyspace notifications 来触发 <<`SessionDeletedEvent`,api-redisindexedsessionrepository-sessiondestroyedevent>> 和 <<`SessionExpiredEvent`,api-redisindexedsessionrepository-sessiondestroyedevent>>。SessionDeletedEvent
或 SessionExpiredEvent
确保与 Session
相关联的资源已被清除。例如,当你使用 Spring Session 的 WebSocket 支持时,Redis 过期或删除事件会触发与会话相关联的任何 WebSocket 连接关闭。
Spring Session relies on the delete and expired keyspace notifications from Redis to fire a <<`SessionDeletedEvent`,api-redisindexedsessionrepository-sessiondestroyedevent>> and a <<`SessionExpiredEvent`,api-redisindexedsessionrepository-sessiondestroyedevent>>, respectively.
SessionDeletedEvent
or SessionExpiredEvent
ensure that resources associated with the Session
are cleaned up.
For example, when you use Spring Session’s WebSocket support, the Redis expired or delete event triggers any WebSocket connections associated with the session to be closed.
过期时间没有直接跟踪在会话键本身上,因为这意味着会话数据将不可用。相反,使用了一个特殊会话过期时间键。在前一个示例中,过期时间键如下所示:
Expiration is not tracked directly on the session key itself, since this would mean the session data would no longer be available. Instead, a special session expires key is used. In the preceding example, the expires key is as follows:
APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe "" EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800
当会话过期时间键被删除或过期时,键空间通知触发对实际会话的查找,并触发 SessionDestroyedEvent
。
When a session expires key is deleted or expires, the keyspace notification triggers a lookup of the actual session, and a SessionDestroyedEvent
is fired.
仅仅依赖 Redis 过期有一个问题,如果未访问过该键,Redis 将不保证在何时触发过期事件。具体来说,Redis 用于清除过期键的后台任务是一个低优先级任务,并且可能不会触发键过期。更多详细信息,请参阅 Redis 文档中的 Timing of Expired Events 部分。
One problem with relying on Redis expiration exclusively is that, if the key has not been accessed, Redis makes no guarantee of when the expired event is fired. Specifically, the background task that Redis uses to clean up expired keys is a low-priority task and may not trigger the key expiration. For additional details, see the Timing of Expired Events section in the Redis documentation.
为了规避无法保证发生到期事件的情况,我们可以确保在预期到期时访问每个键。这意味着,如果该键上的 TTL 已过期,则当我们尝试访问该键时,Redis 会删除该键并触发到期事件。
To circumvent the fact that expired events are not guaranteed to happen, we can ensure that each key is accessed when it is expected to expire. This means that, if the TTL is expired on the key, Redis removes the key and fires the expired event when we try to access the key.
出于此原因,还会将每个会话到期时间跟踪到最近的分钟。这允许一个后台任务访问可能已过期的会话,以确保 Redis 到期事件以更确定方式触发。以下示例展示了这些事件:
For this reason, each session expiration is also tracked to the nearest minute. This lets a background task access the potentially expired sessions to ensure that Redis expired events are fired in a more deterministic fashion. The following example shows these events:
SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe EXPIRE spring:session:expirations1439245080000 2100
然后,后台任务将这些映射用于显式请求每个键。通过访问键而不是删除它,我们可以确保 Redis 仅在 TTL 过期时为我们删除该键。
The background task then uses these mappings to explicitly request each key. By accessing the key, rather than deleting it, we ensure that Redis deletes the key for us only if the TTL is expired.
我们没有明确删除这些键,因为在某些情况下,当键没有过期时,可能会存在错误地将键标识为过期的竞争条件。除了使用分布式锁(这会降低我们的性能)外,没有办法确保过期映射的一致性。通过简单地访问密钥,我们确保仅在该密钥上的 TTL 过期时才移除该密钥。 |
We do not explicitly delete the keys, since, in some instances, there may be a race condition that incorrectly identifies a key as expired when it is not. Short of using distributed locks (which would kill our performance), there is no way to ensure the consistency of the expiration mapping. By simply accessing the key, we ensure that the key is only removed if the TTL on that key is expired. |
SessionDeletedEvent
and SessionExpiredEvent
SessionDeletedEvent
和 SessionExpiredEvent
都是 SessionDestroyedEvent
的类型。
SessionDeletedEvent
and SessionExpiredEvent
are both types of SessionDestroyedEvent
.
RedisIndexedSessionRepository
支持在删除 Session
时触发 SessionDeletedEvent
,或在 Session
过期时触发 SessionExpiredEvent
。这对于确保与 Session
关联的资源得到妥善清理是必要的。
RedisIndexedSessionRepository
supports firing a SessionDeletedEvent
when a Session
is deleted or a SessionExpiredEvent
when a Session
expires.
This is necessary to ensure resources associated with the Session
are properly cleaned up.
例如,在与 WebSocket 集成时,SessionDestroyedEvent
负责关闭任何活动的 WebSocket 连接。
For example, when integrating with WebSockets, the SessionDestroyedEvent
is in charge of closing any active WebSocket connections.
通过监听 Redis Keyspace events 的 SessionMessageListener
提供触发 SessionDeletedEvent
或 SessionExpiredEvent
的功能。为了使其工作,需要启用 Generic 命令和 Expired 事件的 Redis Keyspace 事件。以下示例演示如何执行此操作:
Firing SessionDeletedEvent
or SessionExpiredEvent
is made available through the SessionMessageListener
, which listens to Redis Keyspace events.
In order for this to work, Redis Keyspace events for Generic commands and Expired events needs to be enabled.
The following example shows how to do so:
redis-cli config set notify-keyspace-events Egx
如果你使用 @EnableRedisHttpSession(enableIndexingAndEvents = true)
,则将自动管理 SessionMessageListener
并启用必要的 Redis 密钥空间事件。但是,在受保护的 Redis 环境中,将禁用 config 命令。这意味着 Spring Session 无法为你配置 Redis 密钥空间事件。要禁用自动配置,可添加 ConfigureRedisAction.NO_OP
作为 bean。
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.
例如,使用 Java 配置时,你可以使用以下内容:
For example, with Java configuration, you can use the following:
/*
* 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 docs;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.SubscriptionListener;
import org.springframework.session.data.redis.config.ConfigureRedisAction;
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;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock;
/**
* @author Rob Winch
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
class RedisHttpSessionConfigurationNoOpConfigureRedisActionTests {
@Test
void redisConnectionFactoryNotUsedSinceNoValidation() {
}
@EnableRedisHttpSession
@Configuration
static class Config {
// tag::configure-redis-action[]
@Bean
ConfigureRedisAction configureRedisAction() {
return ConfigureRedisAction.NO_OP;
}
// end::configure-redis-action[]
@Bean
RedisConnectionFactory redisConnectionFactory() {
RedisConnectionFactory connectionFactoryMock = mock(RedisConnectionFactory.class);
RedisConnection connectionMock = mock(RedisConnection.class);
given(connectionFactoryMock.getConnection()).willReturn(connectionMock);
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;
}
}
}
在 XML 配置中,你可以使用以下内容:
In XML configuration, you can use the following:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util-4.1.xsd">
<context:annotation-config/>
<bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisIndexedHttpSessionConfiguration"/>
<!-- tag::configure-redis-action[] -->
<util:constant
static-field="org.springframework.session.data.redis.config.ConfigureRedisAction.NO_OP"/>
<!-- end::configure-redis-action[] -->
<bean class="docs.HttpSessionConfigurationNoOpConfigureRedisActionXmlTests"
factory-method="connectionFactory"/>
</beans>
Using SessionCreatedEvent
创建会话后,将向 Redis 发送一个事件,其通道 ID 为 spring:session:channel:created:33fdd1b6-b496-4b33-9f7d-df96679d32fe
,其中 33fdd1b6-b496-4b33-9f7d-df96679d32fe
是会话 ID。事件正文是已创建的会话。
When a session is created, an event is sent to Redis with a channel ID of spring:session:channel:created:33fdd1b6-b496-4b33-9f7d-df96679d32fe
,
where 33fdd1b6-b496-4b33-9f7d-df96679d32fe
is the session ID. The body of the event is the session that was created.
如果注册为 MessageListener
(默认),则 RedisIndexedSessionRepository
随后将 Redis 消息转换为 SessionCreatedEvent
。
If registered as a MessageListener
(the default), RedisIndexedSessionRepository
then translates the Redis message into a SessionCreatedEvent
.
Viewing the Session in Redis
在 installing redis-cli 之后,你可以检查 Redis using the redis-cli 中的值。例如,你可以在一个终端中输入以下内容:redis-cli HGETALL spring:session:events:1
After installing redis-cli, you can inspect the values in Redis using the redis-cli. For example, you can enter the following into a terminal:
$ redis-cli
redis 127.0.0.1:6379> keys *
1) "spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021" 1
2) "spring:session:expirations:1418772300000" 2
1 | The suffix of this key is the session identifier of the Spring Session. |
2 | This key contains all the session IDs that should be deleted at the time 1418772300000 . |
你还可以查看每个会话的属性。以下示例展示了如何执行此操作:
You can also view the attributes of each session. The following example shows how to do so:
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"
Using ReactiveRedisSessionRepository
ReactiveRedisSessionRepository
是使用 Spring Data 的 ReactiveRedisOperations
实现的 ReactiveSessionRepository
。在 Web 环境中,这通常与 WebSessionStore
一起使用。
ReactiveRedisSessionRepository
is a ReactiveSessionRepository
that is implemented by using Spring Data’s ReactiveRedisOperations
.
In a web environment, this is typically used in combination with WebSessionStore
.
Instantiating a ReactiveRedisSessionRepository
以下示例展示了如何创建新实例:
The following example shows how to create a new instance:
/*
* Copyright 2014-2023 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 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;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.mock.web.MockServletContext;
import org.springframework.session.MapSession;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.RedisSessionRepository;
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Rob Winch
* @author Vedran Pavic
*/
class IndexDocTests {
private static final String ATTR_USER = "user";
@Test
void repositoryDemo() {
RepositoryDemo<MapSession> demo = new RepositoryDemo<>();
demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());
demo.demo();
}
// tag::repository-demo[]
public class RepositoryDemo<S extends Session> {
private SessionRepository<S> repository; (1)
public void demo() {
S toSave = this.repository.createSession(); (2)
(3)
User rwinch = new User("rwinch");
toSave.setAttribute(ATTR_USER, rwinch);
this.repository.save(toSave); (4)
S session = this.repository.findById(toSave.getId()); (5)
(6)
User user = session.getAttribute(ATTR_USER);
assertThat(user).isEqualTo(rwinch);
}
// ... setter methods ...
}
// end::repository-demo[]
@Test
void expireRepositoryDemo() {
ExpiringRepositoryDemo<MapSession> demo = new ExpiringRepositoryDemo<>();
demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());
demo.demo();
}
// tag::expire-repository-demo[]
public class ExpiringRepositoryDemo<S extends Session> {
private SessionRepository<S> repository; (1)
public void demo() {
S toSave = this.repository.createSession(); (2)
// ...
toSave.setMaxInactiveInterval(Duration.ofSeconds(30)); (3)
this.repository.save(toSave); (4)
S session = this.repository.findById(toSave.getId()); (5)
// ...
}
// ... setter methods ...
}
// end::expire-repository-demo[]
@Test
void newRedisSessionRepository() {
// tag::new-redissessionrepository[]
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// ... configure redisTemplate ...
SessionRepository<? extends Session> repository = new RedisSessionRepository(redisTemplate);
// end::new-redissessionrepository[]
}
@Test
void newRedisIndexedSessionRepository() {
// tag::new-redisindexedsessionrepository[]
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// ... configure redisTemplate ...
SessionRepository<? extends Session> repository = new RedisIndexedSessionRepository(redisTemplate);
// end::new-redisindexedsessionrepository[]
}
@Test
void newReactiveRedisSessionRepository() {
LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory();
RedisSerializationContext<String, Object> serializationContext = RedisSerializationContext
.<String, Object>newSerializationContext(new JdkSerializationRedisSerializer())
.build();
// tag::new-reactiveredissessionrepository[]
// ... create and configure connectionFactory and serializationContext ...
ReactiveRedisTemplate<String, Object> redisTemplate = new ReactiveRedisTemplate<>(connectionFactory,
serializationContext);
ReactiveSessionRepository<? extends Session> repository = new ReactiveRedisSessionRepository(redisTemplate);
// end::new-reactiveredissessionrepository[]
}
@Test
void mapRepository() {
// tag::new-mapsessionrepository[]
SessionRepository<? extends Session> repository = new MapSessionRepository(new ConcurrentHashMap<>());
// end::new-mapsessionrepository[]
}
@Test
void newJdbcIndexedSessionRepository() {
// tag::new-jdbcindexedsessionrepository[]
JdbcTemplate jdbcTemplate = new JdbcTemplate();
// ... configure jdbcTemplate ...
TransactionTemplate transactionTemplate = new TransactionTemplate();
// ... configure transactionTemplate ...
SessionRepository<? extends Session> repository = new JdbcIndexedSessionRepository(jdbcTemplate,
transactionTemplate);
// end::new-jdbcindexedsessionrepository[]
}
@Test
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() {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(SpringHttpSessionConfig.class);
context.setServletContext(new MockServletContext());
context.refresh();
try {
context.getBean(SessionRepositoryFilter.class);
}
finally {
context.close();
}
}
private static final class User {
private User(String username) {
}
}
}
有关如何创建 ReactiveRedisConnectionFactory
的详细信息,请参见 Spring Data Redis 参考。
For additional information on how to create a ReactiveRedisConnectionFactory
, see the Spring Data Redis Reference.
Using @EnableRedisWebSession
在 Web 环境中,创建新 ReactiveRedisSessionRepository
的最简单方法是使用 @EnableRedisWebSession
。你可以使用以下属性自定义配置:
In a web environment, the simplest way to create a new ReactiveRedisSessionRepository
is to use @EnableRedisWebSession
.
You can use the following attributes to customize the configuration:
-
maxInactiveIntervalInSeconds: The amount of time before the session expires, in seconds
-
redisNamespace: Allows configuring an application specific namespace for the sessions. Redis keys and channel IDs start with q prefix of
<redisNamespace>:
. -
flushMode: Allows specifying when data is written to Redis. The default is only when
save
is invoked onReactiveSessionRepository
. A value ofFlushMode.IMMEDIATE
writes to Redis as soon as possible.
Optimized Writes
由 ReactiveRedisSessionRepository
管理的 Session
实例会跟踪已更改的属性,并仅更新那些属性。这意味着,如果某个属性已写入一次并读取多次,则我们只需写入该属性一次。
The Session
instances managed by ReactiveRedisSessionRepository
keep track of the properties that have changed and updates only those.
This means that, if an attribute is written once and read many times, we need to write that attribute only once.
Viewing the Session in Redis
在 installing redis-cli 之后,你可以检查 Redis using the redis-cli 中的值。例如,可以再一个终端窗口中输入以下命令:redis-cli HGETALL spring:session:sessions:1
After installing redis-cli, you can inspect the values in Redis using the redis-cli. For example, you can enter the following command into a terminal window:
$ 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. |
您还可以通过使用 hkeys
命令来查看每个会话的属性。以下示例显示了如何执行此操作:
You can also view the attributes of each session by using the hkeys
command.
The following example shows how to do so:
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"
Using MapSessionRepository
MapSessionRepository
允许在 Map
中持久化 Session
,密钥为 Session
ID,值为 Session
。您可以将该实现与 ConcurrentHashMap
一起用作一种测试或便利机制。或者,您也可以将它与分布式 Map
实现结合使用。例如,可以将其与 Hazelcast 一起使用。
The MapSessionRepository
allows for persisting Session
in a Map
, with the key being the Session
ID and the value being the Session
.
You can use the implementation with a ConcurrentHashMap
as a testing or convenience mechanism.
Alternatively, you can use it with distributed Map
implementations. For example, it can be used with Hazelcast.
Instantiating MapSessionRepository
以下示例展示了如何创建新实例:
The following example shows how to create a new instance:
/*
* Copyright 2014-2023 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 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;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.mock.web.MockServletContext;
import org.springframework.session.MapSession;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.RedisSessionRepository;
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Rob Winch
* @author Vedran Pavic
*/
class IndexDocTests {
private static final String ATTR_USER = "user";
@Test
void repositoryDemo() {
RepositoryDemo<MapSession> demo = new RepositoryDemo<>();
demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());
demo.demo();
}
// tag::repository-demo[]
public class RepositoryDemo<S extends Session> {
private SessionRepository<S> repository; (1)
public void demo() {
S toSave = this.repository.createSession(); (2)
(3)
User rwinch = new User("rwinch");
toSave.setAttribute(ATTR_USER, rwinch);
this.repository.save(toSave); (4)
S session = this.repository.findById(toSave.getId()); (5)
(6)
User user = session.getAttribute(ATTR_USER);
assertThat(user).isEqualTo(rwinch);
}
// ... setter methods ...
}
// end::repository-demo[]
@Test
void expireRepositoryDemo() {
ExpiringRepositoryDemo<MapSession> demo = new ExpiringRepositoryDemo<>();
demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());
demo.demo();
}
// tag::expire-repository-demo[]
public class ExpiringRepositoryDemo<S extends Session> {
private SessionRepository<S> repository; (1)
public void demo() {
S toSave = this.repository.createSession(); (2)
// ...
toSave.setMaxInactiveInterval(Duration.ofSeconds(30)); (3)
this.repository.save(toSave); (4)
S session = this.repository.findById(toSave.getId()); (5)
// ...
}
// ... setter methods ...
}
// end::expire-repository-demo[]
@Test
void newRedisSessionRepository() {
// tag::new-redissessionrepository[]
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// ... configure redisTemplate ...
SessionRepository<? extends Session> repository = new RedisSessionRepository(redisTemplate);
// end::new-redissessionrepository[]
}
@Test
void newRedisIndexedSessionRepository() {
// tag::new-redisindexedsessionrepository[]
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// ... configure redisTemplate ...
SessionRepository<? extends Session> repository = new RedisIndexedSessionRepository(redisTemplate);
// end::new-redisindexedsessionrepository[]
}
@Test
void newReactiveRedisSessionRepository() {
LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory();
RedisSerializationContext<String, Object> serializationContext = RedisSerializationContext
.<String, Object>newSerializationContext(new JdkSerializationRedisSerializer())
.build();
// tag::new-reactiveredissessionrepository[]
// ... create and configure connectionFactory and serializationContext ...
ReactiveRedisTemplate<String, Object> redisTemplate = new ReactiveRedisTemplate<>(connectionFactory,
serializationContext);
ReactiveSessionRepository<? extends Session> repository = new ReactiveRedisSessionRepository(redisTemplate);
// end::new-reactiveredissessionrepository[]
}
@Test
void mapRepository() {
// tag::new-mapsessionrepository[]
SessionRepository<? extends Session> repository = new MapSessionRepository(new ConcurrentHashMap<>());
// end::new-mapsessionrepository[]
}
@Test
void newJdbcIndexedSessionRepository() {
// tag::new-jdbcindexedsessionrepository[]
JdbcTemplate jdbcTemplate = new JdbcTemplate();
// ... configure jdbcTemplate ...
TransactionTemplate transactionTemplate = new TransactionTemplate();
// ... configure transactionTemplate ...
SessionRepository<? extends Session> repository = new JdbcIndexedSessionRepository(jdbcTemplate,
transactionTemplate);
// end::new-jdbcindexedsessionrepository[]
}
@Test
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() {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(SpringHttpSessionConfig.class);
context.setServletContext(new MockServletContext());
context.refresh();
try {
context.getBean(SessionRepositoryFilter.class);
}
finally {
context.close();
}
}
private static final class User {
private User(String username) {
}
}
}
Using Spring Session and Hazlecast
Hazelcast Sample 是一个完整的应用程序,展示如何将 Spring Session 与 Hazelcast 配合使用。
The Hazelcast Sample is a complete application that demonstrates how to use Spring Session with Hazelcast.
要运行它,请使用以下命令:
To run it, use the following command:
./gradlew :samples:hazelcast:tomcatRun
Hazelcast Spring Sample 是一个完整的应用程序,展示如何将 Spring Session 与 Hazelcast 和 Spring Security 配合使用。
The Hazelcast Spring Sample is a complete application that demonstrates how to use Spring Session with Hazelcast and Spring Security.
它包括支持触发 SessionCreatedEvent
、SessionDeletedEvent
和 SessionExpiredEvent
的示例 Hazelcast MapListener
实现。
It includes example Hazelcast MapListener
implementations that support firing SessionCreatedEvent
, SessionDeletedEvent
, and SessionExpiredEvent
.
要运行它,请使用以下命令:
To run it, use the following command:
./gradlew :samples:hazelcast-spring:tomcatRun
Using ReactiveMapSessionRepository
ReactiveMapSessionRepository
允许在 Map
中持久化 Session
,密钥为 Session
ID,值为 Session
。您可以将该实现与 ConcurrentHashMap
一起用作一种测试或便利机制。或者,您也可以将它与分布式 Map
实现结合使用,但要求所提供的 Map
必须是非阻塞的。
The ReactiveMapSessionRepository
allows for persisting Session
in a Map
, with the key being the Session
ID and the value being the Session
.
You can use the implementation with a ConcurrentHashMap
as a testing or convenience mechanism.
Alternatively, you can use it with distributed Map
implementations, with the requirement that the supplied Map
must be non-blocking.
Using JdbcIndexedSessionRepository
JdbcIndexedSessionRepository
是一个 SessionRepository
实现,它使用 Spring 的 JdbcOperations
将会话存储在关系数据库中。在 Web 环境中,这通常与 SessionRepositoryFilter
结合使用。请注意,此实现不支持发布会话事件。
JdbcIndexedSessionRepository
is a SessionRepository
implementation that uses Spring’s JdbcOperations
to store sessions in a relational database.
In a web environment, this is typically used in combination with SessionRepositoryFilter
.
Note that this implementation does not support publishing of session events.
Instantiating a JdbcIndexedSessionRepository
以下示例展示了如何创建新实例:
The following example shows how to create a new instance:
/*
* Copyright 2014-2023 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 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;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.mock.web.MockServletContext;
import org.springframework.session.MapSession;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.RedisSessionRepository;
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Rob Winch
* @author Vedran Pavic
*/
class IndexDocTests {
private static final String ATTR_USER = "user";
@Test
void repositoryDemo() {
RepositoryDemo<MapSession> demo = new RepositoryDemo<>();
demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());
demo.demo();
}
// tag::repository-demo[]
public class RepositoryDemo<S extends Session> {
private SessionRepository<S> repository; (1)
public void demo() {
S toSave = this.repository.createSession(); (2)
(3)
User rwinch = new User("rwinch");
toSave.setAttribute(ATTR_USER, rwinch);
this.repository.save(toSave); (4)
S session = this.repository.findById(toSave.getId()); (5)
(6)
User user = session.getAttribute(ATTR_USER);
assertThat(user).isEqualTo(rwinch);
}
// ... setter methods ...
}
// end::repository-demo[]
@Test
void expireRepositoryDemo() {
ExpiringRepositoryDemo<MapSession> demo = new ExpiringRepositoryDemo<>();
demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());
demo.demo();
}
// tag::expire-repository-demo[]
public class ExpiringRepositoryDemo<S extends Session> {
private SessionRepository<S> repository; (1)
public void demo() {
S toSave = this.repository.createSession(); (2)
// ...
toSave.setMaxInactiveInterval(Duration.ofSeconds(30)); (3)
this.repository.save(toSave); (4)
S session = this.repository.findById(toSave.getId()); (5)
// ...
}
// ... setter methods ...
}
// end::expire-repository-demo[]
@Test
void newRedisSessionRepository() {
// tag::new-redissessionrepository[]
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// ... configure redisTemplate ...
SessionRepository<? extends Session> repository = new RedisSessionRepository(redisTemplate);
// end::new-redissessionrepository[]
}
@Test
void newRedisIndexedSessionRepository() {
// tag::new-redisindexedsessionrepository[]
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// ... configure redisTemplate ...
SessionRepository<? extends Session> repository = new RedisIndexedSessionRepository(redisTemplate);
// end::new-redisindexedsessionrepository[]
}
@Test
void newReactiveRedisSessionRepository() {
LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory();
RedisSerializationContext<String, Object> serializationContext = RedisSerializationContext
.<String, Object>newSerializationContext(new JdkSerializationRedisSerializer())
.build();
// tag::new-reactiveredissessionrepository[]
// ... create and configure connectionFactory and serializationContext ...
ReactiveRedisTemplate<String, Object> redisTemplate = new ReactiveRedisTemplate<>(connectionFactory,
serializationContext);
ReactiveSessionRepository<? extends Session> repository = new ReactiveRedisSessionRepository(redisTemplate);
// end::new-reactiveredissessionrepository[]
}
@Test
void mapRepository() {
// tag::new-mapsessionrepository[]
SessionRepository<? extends Session> repository = new MapSessionRepository(new ConcurrentHashMap<>());
// end::new-mapsessionrepository[]
}
@Test
void newJdbcIndexedSessionRepository() {
// tag::new-jdbcindexedsessionrepository[]
JdbcTemplate jdbcTemplate = new JdbcTemplate();
// ... configure jdbcTemplate ...
TransactionTemplate transactionTemplate = new TransactionTemplate();
// ... configure transactionTemplate ...
SessionRepository<? extends Session> repository = new JdbcIndexedSessionRepository(jdbcTemplate,
transactionTemplate);
// end::new-jdbcindexedsessionrepository[]
}
@Test
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() {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(SpringHttpSessionConfig.class);
context.setServletContext(new MockServletContext());
context.refresh();
try {
context.getBean(SessionRepositoryFilter.class);
}
finally {
context.close();
}
}
private static final class User {
private User(String username) {
}
}
}
有关如何创建和配置 JdbcTemplate
和 PlatformTransactionManager
的其他信息,请参阅 {docs-url}/spring/docs/{spring-core-version}/spring-framework-reference/data-access.html[Spring Framework Reference Documentation]。
For additional information on how to create and configure JdbcTemplate
and PlatformTransactionManager
, see the {docs-url}/spring/docs/{spring-core-version}/spring-framework-reference/data-access.html[Spring Framework Reference Documentation].
Using @EnableJdbcHttpSession
在 Web 环境中,创建新的 JdbcIndexedSessionRepository
的最简单方法是使用 @EnableJdbcHttpSession
。您可以在 Samples and Guides (Start Here) 中找到完整的示例用法。您可以使用以下属性来自定义配置:
In a web environment, the simplest way to create a new JdbcIndexedSessionRepository
is to use @EnableJdbcHttpSession
.
You can find complete example usage in the Samples and Guides (Start Here)
You can use the following attributes to customize the configuration:
-
tableName: The name of database table used by Spring Session to store sessions
-
maxInactiveIntervalInSeconds: The amount of time before the session will expire in seconds
Customizing LobHandler
您可以通过创建一个名为 springSessionLobHandler
的 bean(该 bean 实现 LobHandler
)来自定义 BLOB 处理。
You can customize BLOB handling by creating a bean named springSessionLobHandler
that implements LobHandler
.
Customizing ConversionService
可以通过提供 ConversionService
实例来自定义会话的默认序列化和反序列化。在典型的 Spring 环境中,会自动选择默认 ConversionService
bean(名为 conversionService
)并将其用于序列化和反序列化。但是,您可以通过提供名为 springSessionConversionService
的 bean 来覆盖默认 ConversionService
。
You can customize the default serialization and deserialization of the session by providing a ConversionService
instance.
When working in a typical Spring environment, the default ConversionService
bean (named conversionService
) is automatically picked up and used for serialization and deserialization.
However, you can override the default ConversionService
by providing a bean named springSessionConversionService
.
Storage Details
默认情况下,该实现使用 SPRING_SESSION
和 SPRING_SESSION_ATTRIBUTES
表来存储会话。请注意,如前所述,您可以自定义表名。在这种情况下,用于存储属性的表会使用提供的表名加上 _ATTRIBUTES
作为后缀来命名。如果需要进一步自定义,您可以使用 set*Query
setter 方法来自定义存储库所使用的 SQL 查询。在这种情况下,您需要手动配置 sessionRepository
bean。
By default, this implementation uses SPRING_SESSION
and SPRING_SESSION_ATTRIBUTES
tables to store sessions.
Note that you can customize the table name, as already described. In that case, the table used to store attributes is named by using the provided table name suffixed with _ATTRIBUTES
.
If further customizations are needed, you can customize the SQL queries used by the repository by using set*Query
setter methods. In this case, you need to manually configure the sessionRepository
bean.
由于各种数据库供应商之间存在差异,尤其是在存储二进制数据时,请确保使用特定于您的数据库的 SQL 脚本。大多数主要数据库供应商的脚本打包为 org/springframework/session/jdbc/schema-.sql
, where 是目标数据库类型。
Due to the differences between the various database vendors, especially when it comes to storing binary data, make sure to use SQL scripts specific to your database.
Scripts for most major database vendors are packaged as org/springframework/session/jdbc/schema-.sql
, where is the target database type.
例如,对于 PostgreSQL,您可以使用以下架构脚本:
For example, with PostgreSQL, you can use the following schema script:
Unresolved include directive in modules/ROOT/pages/api.adoc - include::example$session-jdbc-main-resources-dir/org/springframework/session/jdbc/schema-postgresql.sql[]
对于 MySQL 数据库,您可以使用以下脚本:
With MySQL database, you can use the following script:
Unresolved include directive in modules/ROOT/pages/api.adoc - include::example$session-jdbc-main-resources-dir/org/springframework/session/jdbc/schema-mysql.sql[]
Transaction Management
JdbcIndexedSessionRepository
中的所有 JDBC 操作都是以事务方式执行的。事务的传播设置成 REQUIRES_NEW
来避免因与现有事务发生冲突而导致意外的行为(例如,在一个已参与只读事务的线程中运行 save
操作)。
All JDBC operations in JdbcIndexedSessionRepository
are performed in a transactional manner.
Transactions are performed with propagation set to REQUIRES_NEW
in order to avoid unexpected behavior due to interference with existing transactions (for example, running a save
operation in a thread that already participates in a read-only transaction).
Using HazelcastIndexedSessionRepository
HazelcastIndexedSessionRepository
是一个 SessionRepository
实现,它将会话存储在 Hazelcast 的分布式 IMap
中。在 Web 环境中,这通常与 SessionRepositoryFilter
结合使用。
HazelcastIndexedSessionRepository
is a SessionRepository
implementation that stores sessions in Hazelcast’s distributed IMap
.
In a web environment, this is typically used in combination with SessionRepositoryFilter
.
Instantiating a HazelcastIndexedSessionRepository
以下示例展示了如何创建新实例:
The following example shows how to create a new instance:
/*
* Copyright 2014-2023 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 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;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.mock.web.MockServletContext;
import org.springframework.session.MapSession;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.RedisSessionRepository;
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Rob Winch
* @author Vedran Pavic
*/
class IndexDocTests {
private static final String ATTR_USER = "user";
@Test
void repositoryDemo() {
RepositoryDemo<MapSession> demo = new RepositoryDemo<>();
demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());
demo.demo();
}
// tag::repository-demo[]
public class RepositoryDemo<S extends Session> {
private SessionRepository<S> repository; (1)
public void demo() {
S toSave = this.repository.createSession(); (2)
(3)
User rwinch = new User("rwinch");
toSave.setAttribute(ATTR_USER, rwinch);
this.repository.save(toSave); (4)
S session = this.repository.findById(toSave.getId()); (5)
(6)
User user = session.getAttribute(ATTR_USER);
assertThat(user).isEqualTo(rwinch);
}
// ... setter methods ...
}
// end::repository-demo[]
@Test
void expireRepositoryDemo() {
ExpiringRepositoryDemo<MapSession> demo = new ExpiringRepositoryDemo<>();
demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());
demo.demo();
}
// tag::expire-repository-demo[]
public class ExpiringRepositoryDemo<S extends Session> {
private SessionRepository<S> repository; (1)
public void demo() {
S toSave = this.repository.createSession(); (2)
// ...
toSave.setMaxInactiveInterval(Duration.ofSeconds(30)); (3)
this.repository.save(toSave); (4)
S session = this.repository.findById(toSave.getId()); (5)
// ...
}
// ... setter methods ...
}
// end::expire-repository-demo[]
@Test
void newRedisSessionRepository() {
// tag::new-redissessionrepository[]
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// ... configure redisTemplate ...
SessionRepository<? extends Session> repository = new RedisSessionRepository(redisTemplate);
// end::new-redissessionrepository[]
}
@Test
void newRedisIndexedSessionRepository() {
// tag::new-redisindexedsessionrepository[]
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// ... configure redisTemplate ...
SessionRepository<? extends Session> repository = new RedisIndexedSessionRepository(redisTemplate);
// end::new-redisindexedsessionrepository[]
}
@Test
void newReactiveRedisSessionRepository() {
LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory();
RedisSerializationContext<String, Object> serializationContext = RedisSerializationContext
.<String, Object>newSerializationContext(new JdkSerializationRedisSerializer())
.build();
// tag::new-reactiveredissessionrepository[]
// ... create and configure connectionFactory and serializationContext ...
ReactiveRedisTemplate<String, Object> redisTemplate = new ReactiveRedisTemplate<>(connectionFactory,
serializationContext);
ReactiveSessionRepository<? extends Session> repository = new ReactiveRedisSessionRepository(redisTemplate);
// end::new-reactiveredissessionrepository[]
}
@Test
void mapRepository() {
// tag::new-mapsessionrepository[]
SessionRepository<? extends Session> repository = new MapSessionRepository(new ConcurrentHashMap<>());
// end::new-mapsessionrepository[]
}
@Test
void newJdbcIndexedSessionRepository() {
// tag::new-jdbcindexedsessionrepository[]
JdbcTemplate jdbcTemplate = new JdbcTemplate();
// ... configure jdbcTemplate ...
TransactionTemplate transactionTemplate = new TransactionTemplate();
// ... configure transactionTemplate ...
SessionRepository<? extends Session> repository = new JdbcIndexedSessionRepository(jdbcTemplate,
transactionTemplate);
// end::new-jdbcindexedsessionrepository[]
}
@Test
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() {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(SpringHttpSessionConfig.class);
context.setServletContext(new MockServletContext());
context.refresh();
try {
context.getBean(SessionRepositoryFilter.class);
}
finally {
context.close();
}
}
private static final class User {
private User(String username) {
}
}
}
有关如何创建和配置 Hazelcast 实例的更多信息,请参阅 Hazelcast documentation。
For additional information on how to create and configure Hazelcast instance, see the Hazelcast documentation.
Using @EnableHazelcastHttpSession
若要将 Hazelcast 用作 SessionRepository
的后端源,您可以将 @EnableHazelcastHttpSession
注释添加到一个 @Configuration
类中。这样做将扩展由 @EnableSpringHttpSession
注释提供的功能,但会在 Hazelcast 中为您创建 SessionRepository
。您必须提供一个单一的 HazelcastInstance
bean,让配置可以正常运作。您可以在 Samples and Guides (Start Here) 中找到一个完整的配置示例。
To use Hazelcast as your backing source for the SessionRepository
, you can add the @EnableHazelcastHttpSession
annotation to a @Configuration
class.
Doing so extends the functionality provided by the @EnableSpringHttpSession
annotation but makes the SessionRepository
for you in Hazelcast.
You must provide a single HazelcastInstance
bean for the configuration to work.
You can find a complete configuration example in the Samples and Guides (Start Here).
Basic Customization
您可以在 @EnableHazelcastHttpSession
中使用以下属性来自定义配置:
You can use the following attributes on @EnableHazelcastHttpSession
to customize the configuration:
-
maxInactiveIntervalInSeconds: The amount of time before the session expires, in seconds. The default is 1800 seconds (30 minutes)
-
sessionMapName: The name of the distributed
Map
that is used in Hazelcast to store the session data.
Session Events
使用 MapListener
来响应添加到分布式 Map
中的条目、从分布式 Map
中驱逐条目,以及从分布式 Map
中移除条目,会使这些事件触发通过 ApplicationEventPublisher
发布 SessionCreatedEvent
、SessionExpiredEvent
和 SessionDeletedEvent
事件(分别)。
Using a MapListener
to respond to entries being added, evicted, and removed from the distributed Map
causes these events to trigger publishing of SessionCreatedEvent
, SessionExpiredEvent
, and SessionDeletedEvent
events (respectively) through the ApplicationEventPublisher
.
Storage Details
会话被存储在 Hazelcast 的一个分布式 IMap
中。IMap
接口方法用于 get()
和 put()
会话。此外,values()
方法支持 FindByIndexNameSessionRepository#findByIndexNameAndIndexValue
操作,以及适当的 ValueExtractor
(需要向 Hazelcast 注册)。有关此配置的详细信息,请参阅 Hazelcast Spring Sample。会话在 IMap
中的过期处理方式是由 Hazelcast 支持在条目被 put()
到 IMap
中时设置生存时间来实现的。空闲时间超过生存时间的条目(会话)会自动从 IMap
中移除。
Sessions are stored in a distributed IMap
in Hazelcast.
The IMap
interface methods are used to get()
and put()
Sessions.
Additionally, the values()
method supports a FindByIndexNameSessionRepository#findByIndexNameAndIndexValue
operation, together with appropriate ValueExtractor
(which needs to be registered with Hazelcast). See the Hazelcast Spring Sample for more details on this configuration.
The expiration of a session in the IMap
is handled by Hazelcast’s support for setting the time to live on an entry when it is put()
into the IMap
. Entries (sessions) that have been idle longer than the time to live are automatically removed from the IMap
.
无需配置榛子配置中的 max-idle-seconds
或 time-to-live-seconds
等任何设置。
You should not need to configure any settings such as max-idle-seconds
or time-to-live-seconds
for the IMap
within the Hazelcast configuration.
请注意,如果您使用 Hazelcast 的 MapStore
来持久化会话 IMap
,则在从 MapStore
重新加载会话时将适用以下限制:
Note that if you use Hazelcast’s MapStore
to persist your sessions IMap
, the following limitations apply when reloading the sessions from MapStore
:
-
Reloading triggers
EntryAddedListener
results inSessionCreatedEvent
being re-published -
Reloading uses default TTL for a given
IMap
results in sessions losing their original TTL
Using CookieSerializer
CookieSerializer
负责定义如何写入会话 cookie。Spring Session 提供使用 DefaultCookieSerializer
的默认实现。
A CookieSerializer
is responsible for defining how the session cookie is written.
Spring Session comes with a default implementation using DefaultCookieSerializer
.
Exposing CookieSerializer
as a bean
将 CookieSerializer
公开为 Spring Bean 可在您使用 @EnableRedisHttpSession
等配置时增加现有配置。
Exposing the CookieSerializer
as a Spring bean augments the existing configuration when you use configurations like @EnableRedisHttpSession
.
以下示例演示了如何执行此操作:
The following example shows how to do so:
Unresolved include directive in modules/ROOT/pages/api.adoc - include::example$spring-session-samples/spring-session-sample-javaconfig-custom-cookie/src/main/java/sample/Config.java[]
1 | We customize the name of the cookie to be JSESSIONID . |
2 | We customize the path of the cookie to be / (rather than the default of the context root). |
3 | We customize the domain name pattern (a regular expression) to be ^.?\\.(\\w\\.[a-z]+)$ .
This allows sharing a session across domains and applications.
If the regular expression does not match, no domain is set and the existing domain is used.
If the regular expression matches, the first grouping is used as the domain.
This means that a request to [role="bare"]https://child.example.com sets the domain to example.com .
However, a request to [role="bare"]http://localhost:8080/ or [role="bare"]https://192.168.1.100:8080/ leaves the cookie unset and, thus, still works in development without any changes being necessary for production. |
你应当只匹配有效的域名字符,因为域名会反映在响应中。这样做可以防止恶意用户实施攻击,比如 HTTP Response Splitting。
You should only match on valid domain characters, since the domain name is reflected in the response. Doing so prevents a malicious user from performing such attacks as HTTP Response Splitting.
Customizing CookieSerializer
您可以使用 DefaultCookieSerializer
上的任何以下配置选项自定义如何写入会话 cookie。
You can customize how the session cookie is written by using any of the following configuration options on the DefaultCookieSerializer
.
-
cookieName
: The name of the cookie to use. Default:SESSION
. -
useSecureCookie
: Specifies whether a secure cookie should be used. Default: Use the value ofHttpServletRequest.isSecure()
at the time of creation. -
cookiePath
: The path of the cookie. Default: The context root. -
cookieMaxAge
: Specifies the max age of the cookie to be set at the time the session is created. Default:-1
, which indicates the cookie should be removed when the browser is closed. -
jvmRoute
: Specifies a suffix to be appended to the session ID and included in the cookie. Used to identify which JVM to route to for session affinity. With some implementations (that is, Redis) this option provides no performance benefit. However, it can help with tracing logs of a particular user. -
domainName
: Allows specifying a specific domain name to be used for the cookie. This option is simple to understand but often requires a different configuration between development and production environments. SeedomainNamePattern
as an alternative. -
domainNamePattern
: A case-insensitive pattern used to extract the domain name from theHttpServletRequest#getServerName()
. The pattern should provide a single grouping that is used to extract the value of the cookie domain. If the regular expression does not match, no domain is set and the existing domain is used. If the regular expression matches, the first grouping is used as the domain. -
sameSite
: The value for theSameSite
cookie directive. To disable the serialization of theSameSite
cookie directive, you may set this value tonull
. Default:Lax
你应当只匹配有效的域名字符,因为域名会反映在响应中。这样做可以防止恶意用户实施攻击,比如 HTTP Response Splitting。
You should only match on valid domain characters, since the domain name is reflected in the response. Doing so prevents a malicious user from performing such attacks as HTTP Response Splitting.
Customizing SessionRepository
实现一个自定义 <<`SessionRepository`,api-sessionrepository>> API 应该是一个相当简单的任务。将自定义实现与 <<`@EnableSpringHttpSession`,api-enablespringhttpsession>> 支持结合起来,可以让您重用现有的 Spring Session 配置设施和基础结构。然而,有几个方面值得仔细考虑。
Implementing a custom <<`SessionRepository`,api-sessionrepository>> API should be a fairly straightforward task. Coupling the custom implementation with <<`@EnableSpringHttpSession`,api-enablespringhttpsession>> support lets you reuse existing Spring Session configuration facilities and infrastructure. There are, however, a couple of aspects that deserve closer consideration.
在 HTTP 请求的生命周期中,HttpSession
通常会持久化到 SessionRepository
中两次。第一个持久化操作是为了确保会话在客户端可以访问会话 ID 时可供客户端使用,并且在提交会话后也需要写入,因为可能会对会话进行进一步修改。考虑到这一点,我们通常建议 SessionRepository
实现跟踪更改以确保只保存增量。这在高并发环境中尤为重要,在该环境中,多个请求对同一 HttpSession
进行操作,因此会引起竞争条件,导致请求相互覆盖对会话属性的更改。Spring Session 提供的所有 SessionRepository
实现都使用所描述的方法来持久化会话更改,可在您实现自定义 SessionRepository
时作为指导。
During the lifecycle of an HTTP request, the HttpSession
is typically persisted to SessionRepository
twice.
The first persist operation is to ensure that the session is available to the client as soon as the client has access to the session ID, and it is also necessary to write after the session is committed because further modifications to the session might be made.
Having this in mind, we generally recommend that a SessionRepository
implementation keep track of changes to ensure that only deltas are saved.
This is particularly important in highly concurrent environments, where multiple requests operate on the same HttpSession
and, therefore, cause race conditions, with requests overriding each other’s changes to session attributes.
All of the SessionRepository
implementations provided by Spring Session use the described approach to persist session changes and can be used for guidance when you implement custom SessionRepository
.
请注意,同样的建议同样适用于实现自定义 <<`ReactiveSessionRepository`,api-reactivesessionrepository>>。在这种情况下,您应该使用 <<`@EnableSpringWebSession`,api-enablespringwebsession>>。
Note that the same recommendations apply for implementing a custom <<`ReactiveSessionRepository`,api-reactivesessionrepository>> as well. In this case, you should use the <<`@EnableSpringWebSession`,api-enablespringwebsession>>.