API Documentation
在线浏览完整的 {docs-url}/spring-session/docs/{spring-session-version}/api/[Javadoc]。在以下各节中描述了关键的 API:
Using Session
Session
是名称值对的一个简化 Map
。
典型用法可能如下面的清单所示:
/*
* 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 | 我们创建一个 SessionRepository 实例与泛型类型 S ,它扩展了 Session 。泛型类型在我们的类中定义。 |
2 | 我们使用 SessionRepository 创建一个新的 Session ,并将其分配给类型为 S 的变量。 |
3 | 我们与 Session 交互。在我们的示例中,我们演示将 User 保存到 Session 。 |
4 | 现在我们保存 Session 。这就是为什么我们需要泛型类型 S 的原因。SessionRepository 只允许保存使用相同 SessionRepository 创建或检索的 Session 实例。这允许 SessionRepository 进行特定于实现的优化(即只写入已更改的属性)。 |
5 | 我们从 SessionRepository 中检索 Session 。 |
6 | 我们从 Session 中获取持久化的 User ,而无需显式转换我们的属性。 |
Session
API 还提供了与 Session
实例过期相关的属性。
典型用法可能如下面的清单所示:
/*
* 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 | 我们创建一个 SessionRepository 实例与泛型类型 S ,它扩展了 Session 。泛型类型在我们的类中定义。 |
2 | 我们使用 SessionRepository 创建一个新的 Session ,并将其分配给类型为 S 的变量。 |
3 | 我们与`Session`交互。在示例中,我们演示了可在`Session`过期之前更新其处于非活动状态的时间量。 |
4 | 我们现在保存`Session`。这就是我们为何需要泛型类型`S`的原因。`SessionRepository`仅允许保存使用相同的`SessionRepository`创建或检索的`Session`实例。这允许`SessionRepository`进行特定于实现的优化(即仅写入已更改的属性)。当保存`Session`时,最后一次访问时间会自动更新。 |
5 | 从`SessionRepository`检索`Session`。如果`Session`已过期,结果将为 null。 |
Using SessionRepository
SessionRepository
负责创建、检索和持久化 Session
实例。
如果可能,您不应直接与 SessionRepository
或 Session
进行交互。相反,开发人员应更倾向于通过 HttpSession
和 WebSocket 集成间接与 SessionRepository
和 Session
进行交互。
Using FindByIndexNameSessionRepository
Spring Session 用于使用 Session
的最基本 API 是 SessionRepository
。此 API 故意非常简单,以便你可以轻松地提供具有基本功能的其他实现。
某些 SessionRepository
实现还可能选择实现 FindByIndexNameSessionRepository
。例如,Spring 的 Redis、JDBC 和 Hazelcast 支持库都实现了 FindByIndexNameSessionRepository
。
FindByIndexNameSessionRepository
提供了一个方法来查找具有给定索引名称和索引值的所有会话。作为所有提供的 FindByIndexNameSessionRepository
实现都支持的常见用例,你可以使用一个方便的方法来查找特定用户的会话。这通过确保名称为 FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME
的会话属性填充了用户名来完成。确保属性已填充是你的责任,因为 Spring Session 不知道正在使用的认证机制。以下清单中演示了如何使用此功能的示例:
/*
* 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[]
}
}
|
对会话编制索引后,你可以使用类似于以下代码找到会话:
/*
* 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
实例。
如果可能,您不应直接与 ReactiveSessionRepository
或 Session
进行交互。相反,您应更倾向于通过 WebSession 集成间接与 ReactiveSessionRepository
和 Session
进行交互。
Using @EnableSpringHttpSession
你可以将 @EnableSpringHttpSession
注解添加到 @Configuration
类中,以将 SessionRepositoryFilter
公开为名为 springSessionRepositoryFilter
的 bean。要使用此注解,你必须提供一个 SessionRepository
bean。以下示例演示了如何操作:
/*
* 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[]
请注意,为你配置的会话过期没有基础设施。这是因为诸如会话过期等事项高度依赖于实现方式。这意味着,如果你需要清理过期的会话,则负责清理过期的会话。
Using @EnableSpringWebSession
你可以将 @EnableSpringWebSession
注解添加到 @Configuration
类中,以将 WebSessionManager
公开为名为 webSessionManager
的 bean。要使用此注解,你必须提供一个 ReactiveSessionRepository
bean。以下示例演示了如何操作:
/*
* 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[]
请注意,为你配置的会话过期没有基础设施。这是因为诸如会话过期等事项高度依赖于实现方式。这意味着,如果你需要清理过期的会话,则负责清理过期的会话。
Using RedisSessionRepository
RedisSessionRepository
是一个 SessionRepository
,通过使用 Spring Data 的 RedisOperations
实现。在 Web 环境中,此实现通常与 SessionRepositoryFilter
结合使用。请注意,此实现不支持会话事件的发布。
Instantiating a RedisSessionRepository
以下列表中展示了如何创建新实例的典型示例:
/*
* 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 参考。
Using @EnableRedisHttpSession
在 Web 环境中,创建新的 RedisSessionRepository
的最简单方法是使用 @EnableRedisHttpSession
。您可以在 Samples and Guides (Start Here) 中找到完整的示例用法。您可以使用以下属性来自定义配置:
enableIndexingAndEvents* enableIndexingAndEvents:是否使用 RedisIndexedSessionRepository
而不是 RedisSessionRepository
。默认值是 false
。* maxInactiveIntervalInSeconds:会话过期之前的时间,以秒为单位。* redisNamespace:允许为会话配置特定于应用程序的命名空间。Redis 密钥和信道 ID 以 <redisNamespace>:
的前缀开头。* flushMode:允许指定何时将数据写入 Redis。默认值仅在 SessionRepository
中调用 save
时写入。FlushMode.IMMEDIATE
值会尽快写入 Redis。
Viewing the Session in Redis
在 installing redis-cli 之后,你可以检查 Redis using the redis-cli 中的值。例如,可以再一个终端窗口中输入以下命令:redis-cli HGETALL spring:session:sessions:1
$ redis-cli
redis 127.0.0.1:6379> keys *
1) "spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021" 1
1 | 此密钥的后缀是 Spring 会话的会话标识符。 |
您还可以通过使用 hkeys
命令来查看每个会话的属性。以下示例显示了如何执行此操作:
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
。
Instantiating a RedisIndexedSessionRepository
以下列表中展示了如何创建新实例的典型示例:
/*
* 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 参考。
Using @EnableRedisHttpSession(enableIndexingAndEvents = true)
在 Web 环境中,创建新的 RedisIndexedSessionRepository
的最简单方法是使用 @EnableRedisHttpSession(enableIndexingAndEvents = true)
。您可以在 Samples and Guides (Start Here) 中找到完整的示例用法。您可以使用以下属性来自定义配置:
-
enableIndexingAndEvents: 是否使用`RedisIndexedSessionRepository`而不是`RedisSessionRepository`。默认值为`false`。
-
maxInactiveIntervalInSeconds: 会话过期的时间量(以秒为单位)。
-
redisNamespace: 允许为会话配置特定于应用程序的命名空间。Redis 密钥和通道 ID 以`<redisNamespace>:`的前缀开头。
-
flushMode: 允许指定何时将数据写入 Redis。默认值为仅当在`SessionRepository`上调用`save`时。值为`FlushMode.IMMEDIATE`尽可能快地写入 Redis。
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]。
Storage Details
以下部分概述了 Redis 如何针对每个操作进行更新。以下示例显示了创建新会话的示例:
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
后续部分描述了详细信息。
Saving a Session
每个会话都作为一个 Hash
存储在 Redis 中。每个会话都使用 HMSET
命令进行设置和更新。以下示例展示了每个会话是如何存储的:
HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 \ maxInactiveInterval 1800 \ lastAccessedTime 1404360000000 \ sessionAttr:attrName someAttrValue \ sessionAttr:attrName2 someAttrValue2
在前面的示例中,关于会话存在以下语句:
-
会话 ID 为 33fdd1b6-b496-4b33-9f7d-df96679d32fe。
-
会话创建于 1404360000000(从 1/1/1970 GMT 的午夜开始的毫秒数)。
-
会话在 1800 秒(30 分钟)后过期。
-
会话最后一次访问时间为 1404360000000(从 1/1/1970 GMT 的午夜开始的毫秒数)。
-
会话有两个属性。第一个是`attrName`,值是`someAttrValue`。第二个会话属性名为`attrName2`,值是`someAttrValue2`。
Optimized Writes
由 RedisIndexedSessionRepository
管理的 Session
实例跟踪改变的属性,只更新那些属性。这意味着如果一个属性写一次并读多次,我们只需要写一次该属性。例如,假设前一部分的 lsiting 中的 attrName2
会话属性被更新。在保存时将运行以下命令:
HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe sessionAttr:attrName2 newValue
Session Expiration
一个过期时间使用 EXPIRE
命令与每个会话相关联,根据 Session.getMaxInactiveInterval()
设置。以下示例显示了一个典型的 EXPIRE
命令:
EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100
请注意,在会话实际过期后的五分钟设置为过期时间。这是必要的,以便在会话过期时可以访问会话的值。一个过期时间在会话本身在实际过期后的五分钟设置,以确保它被清除,但仅在我们执行任何必要处理之后。
|
Spring Session 依赖 Redis 中的删除和已过期的 keyspace notifications 来触发 <<`SessionDeletedEvent`,api-redisindexedsessionrepository-sessiondestroyedevent>> 和 <<`SessionExpiredEvent`,api-redisindexedsessionrepository-sessiondestroyedevent>>。SessionDeletedEvent
或 SessionExpiredEvent
确保与 Session
相关联的资源已被清除。例如,当你使用 Spring Session 的 WebSocket 支持时,Redis 过期或删除事件会触发与会话相关联的任何 WebSocket 连接关闭。
过期时间没有直接跟踪在会话键本身上,因为这意味着会话数据将不可用。相反,使用了一个特殊会话过期时间键。在前一个示例中,过期时间键如下所示:
APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe "" EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800
当会话过期时间键被删除或过期时,键空间通知触发对实际会话的查找,并触发 SessionDestroyedEvent
。
仅仅依赖 Redis 过期有一个问题,如果未访问过该键,Redis 将不保证在何时触发过期事件。具体来说,Redis 用于清除过期键的后台任务是一个低优先级任务,并且可能不会触发键过期。更多详细信息,请参阅 Redis 文档中的 Timing of Expired Events 部分。
为了规避无法保证发生到期事件的情况,我们可以确保在预期到期时访问每个键。这意味着,如果该键上的 TTL 已过期,则当我们尝试访问该键时,Redis 会删除该键并触发到期事件。
出于此原因,还会将每个会话到期时间跟踪到最近的分钟。这允许一个后台任务访问可能已过期的会话,以确保 Redis 到期事件以更确定方式触发。以下示例展示了这些事件:
SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe EXPIRE spring:session:expirations1439245080000 2100
然后,后台任务将这些映射用于显式请求每个键。通过访问键而不是删除它,我们可以确保 Redis 仅在 TTL 过期时为我们删除该键。
我们没有明确删除这些键,因为在某些情况下,当键没有过期时,可能会存在错误地将键标识为过期的竞争条件。除了使用分布式锁(这会降低我们的性能)外,没有办法确保过期映射的一致性。通过简单地访问密钥,我们确保仅在该密钥上的 TTL 过期时才移除该密钥。 |
SessionDeletedEvent
and SessionExpiredEvent
SessionDeletedEvent
和 SessionExpiredEvent
都是 SessionDestroyedEvent
的类型。
RedisIndexedSessionRepository
支持在删除 Session
时触发 SessionDeletedEvent
,或在 Session
过期时触发 SessionExpiredEvent
。这对于确保与 Session
关联的资源得到妥善清理是必要的。
例如,在与 WebSocket 集成时,SessionDestroyedEvent
负责关闭任何活动的 WebSocket 连接。
通过监听 Redis Keyspace events 的 SessionMessageListener
提供触发 SessionDeletedEvent
或 SessionExpiredEvent
的功能。为了使其工作,需要启用 Generic 命令和 Expired 事件的 Redis Keyspace 事件。以下示例演示如何执行此操作:
redis-cli config set notify-keyspace-events Egx
如果你使用 @EnableRedisHttpSession(enableIndexingAndEvents = true)
,则将自动管理 SessionMessageListener
并启用必要的 Redis 密钥空间事件。但是,在受保护的 Redis 环境中,将禁用 config 命令。这意味着 Spring Session 无法为你配置 Redis 密钥空间事件。要禁用自动配置,可添加 ConfigureRedisAction.NO_OP
作为 bean。
例如,使用 Java 配置时,你可以使用以下内容:
/*
* 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 配置中,你可以使用以下内容:
<?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。事件正文是已创建的会话。
如果注册为 MessageListener
(默认),则 RedisIndexedSessionRepository
随后将 Redis 消息转换为 SessionCreatedEvent
。
Viewing the Session in Redis
在 installing redis-cli 之后,你可以检查 Redis using the redis-cli 中的值。例如,你可以在一个终端中输入以下内容:redis-cli HGETALL spring:session:events:1
$ 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 | 此密钥的后缀是 Spring 会话的会话标识符。 |
2 | 此密钥包含所有应在时间`1418772300000`删除的会话 ID。 |
你还可以查看每个会话的属性。以下示例展示了如何执行此操作:
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
一起使用。
Instantiating a ReactiveRedisSessionRepository
以下示例展示了如何创建新实例:
/*
* 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 参考。
Using @EnableRedisWebSession
在 Web 环境中,创建新 ReactiveRedisSessionRepository
的最简单方法是使用 @EnableRedisWebSession
。你可以使用以下属性自定义配置:
-
maxInactiveIntervalInSeconds: 会话过期的时间量(以秒为单位)
-
redisNamespace: 允许为会话配置特定于应用程序的命名空间。Redis 密钥和通道 ID 以`<redisNamespace>:`的前缀开头。
-
flushMode: 允许指定何时将数据写入 Redis。默认值为仅当在`ReactiveSessionRepository`上调用`save`时。值为`FlushMode.IMMEDIATE`尽可能快地写入 Redis。
Viewing the Session in Redis
在 installing redis-cli 之后,你可以检查 Redis using the redis-cli 中的值。例如,可以再一个终端窗口中输入以下命令:redis-cli HGETALL spring:session:sessions:1
$ redis-cli
redis 127.0.0.1:6379> keys *
1) "spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021" 1
1 | 此密钥的后缀是 Spring 会话的会话标识符。 |
您还可以通过使用 hkeys
命令来查看每个会话的属性。以下示例显示了如何执行此操作:
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 一起使用。
Instantiating MapSessionRepository
以下示例展示了如何创建新实例:
/*
* 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 配合使用。
要运行它,请使用以下命令:
./gradlew :samples:hazelcast:tomcatRun
Hazelcast Spring Sample 是一个完整的应用程序,展示如何将 Spring Session 与 Hazelcast 和 Spring Security 配合使用。
它包括支持触发 SessionCreatedEvent
、SessionDeletedEvent
和 SessionExpiredEvent
的示例 Hazelcast MapListener
实现。
要运行它,请使用以下命令:
./gradlew :samples:hazelcast-spring:tomcatRun
Using ReactiveMapSessionRepository
ReactiveMapSessionRepository
允许在 Map
中持久化 Session
,密钥为 Session
ID,值为 Session
。您可以将该实现与 ConcurrentHashMap
一起用作一种测试或便利机制。或者,您也可以将它与分布式 Map
实现结合使用,但要求所提供的 Map
必须是非阻塞的。
Using JdbcIndexedSessionRepository
JdbcIndexedSessionRepository
是一个 SessionRepository
实现,它使用 Spring 的 JdbcOperations
将会话存储在关系数据库中。在 Web 环境中,这通常与 SessionRepositoryFilter
结合使用。请注意,此实现不支持发布会话事件。
Instantiating a JdbcIndexedSessionRepository
以下示例展示了如何创建新实例:
/*
* 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]。
Using @EnableJdbcHttpSession
在 Web 环境中,创建新的 JdbcIndexedSessionRepository
的最简单方法是使用 @EnableJdbcHttpSession
。您可以在 Samples and Guides (Start Here) 中找到完整的示例用法。您可以使用以下属性来自定义配置:
-
tableName: Spring 会话使用的数据库表名称,用于存储会话
-
maxInactiveIntervalInSeconds: 会话在多少秒后过期的时间量
Storage Details
默认情况下,该实现使用 SPRING_SESSION
和 SPRING_SESSION_ATTRIBUTES
表来存储会话。请注意,如前所述,您可以自定义表名。在这种情况下,用于存储属性的表会使用提供的表名加上 _ATTRIBUTES
作为后缀来命名。如果需要进一步自定义,您可以使用 set*Query
setter 方法来自定义存储库所使用的 SQL 查询。在这种情况下,您需要手动配置 sessionRepository
bean。
由于各种数据库供应商之间存在差异,尤其是在存储二进制数据时,请确保使用特定于您的数据库的 SQL 脚本。大多数主要数据库供应商的脚本打包为 org/springframework/session/jdbc/schema-.sql
, where 是目标数据库类型。
例如,对于 PostgreSQL,您可以使用以下架构脚本:
Unresolved include directive in modules/ROOT/pages/api.adoc - include::example$session-jdbc-main-resources-dir/org/springframework/session/jdbc/schema-postgresql.sql[]
对于 MySQL 数据库,您可以使用以下脚本:
Unresolved include directive in modules/ROOT/pages/api.adoc - include::example$session-jdbc-main-resources-dir/org/springframework/session/jdbc/schema-mysql.sql[]
Using HazelcastIndexedSessionRepository
HazelcastIndexedSessionRepository
是一个 SessionRepository
实现,它将会话存储在 Hazelcast 的分布式 IMap
中。在 Web 环境中,这通常与 SessionRepositoryFilter
结合使用。
Instantiating a HazelcastIndexedSessionRepository
以下示例展示了如何创建新实例:
/*
* 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。
Using @EnableHazelcastHttpSession
若要将 Hazelcast 用作 SessionRepository
的后端源,您可以将 @EnableHazelcastHttpSession
注释添加到一个 @Configuration
类中。这样做将扩展由 @EnableSpringHttpSession
注释提供的功能,但会在 Hazelcast 中为您创建 SessionRepository
。您必须提供一个单一的 HazelcastInstance
bean,让配置可以正常运作。您可以在 Samples and Guides (Start Here) 中找到一个完整的配置示例。
Basic Customization
您可以在 @EnableHazelcastHttpSession
中使用以下属性来自定义配置:
-
maxInactiveIntervalInSeconds: 会话过期的时间量(以秒为单位)。默认值为 1800 秒(30 分钟)
-
sessionMapName: Hazelcast 中用于存储会话数据的分布式`Map`的名称。
Session Events
使用 MapListener
来响应添加到分布式 Map
中的条目、从分布式 Map
中驱逐条目,以及从分布式 Map
中移除条目,会使这些事件触发通过 ApplicationEventPublisher
发布 SessionCreatedEvent
、SessionExpiredEvent
和 SessionDeletedEvent
事件(分别)。
Storage Details
会话被存储在 Hazelcast 的一个分布式 IMap
中。IMap
接口方法用于 get()
和 put()
会话。此外,values()
方法支持 FindByIndexNameSessionRepository#findByIndexNameAndIndexValue
操作,以及适当的 ValueExtractor
(需要向 Hazelcast 注册)。有关此配置的详细信息,请参阅 Hazelcast Spring Sample。会话在 IMap
中的过期处理方式是由 Hazelcast 支持在条目被 put()
到 IMap
中时设置生存时间来实现的。空闲时间超过生存时间的条目(会话)会自动从 IMap
中移除。
无需配置榛子配置中的 max-idle-seconds
或 time-to-live-seconds
等任何设置。
请注意,如果您使用 Hazelcast 的 MapStore
来持久化会话 IMap
,则在从 MapStore
重新加载会话时将适用以下限制:
-
重新加载触发器`EntryAddedListener`会导致重新发布`SessionCreatedEvent`
-
重新加载使用给定`IMap`的默认 TTL 会导致会话失去其原始 TTL
Using CookieSerializer
CookieSerializer
负责定义如何写入会话 cookie。Spring Session 提供使用 DefaultCookieSerializer
的默认实现。
Exposing CookieSerializer
as a bean
将 CookieSerializer
公开为 Spring Bean 可在您使用 @EnableRedisHttpSession
等配置时增加现有配置。
以下示例演示了如何执行此操作:
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 | 我们自定义 cookie 名称,使其为 JSESSIONID 。 |
2 | 我们自定义 cookie 路径,使其为 / (而不是默认的上下文根)。 |
3 | 我们自定义域名模式(正则表达式),使其为 ^.?\\.(\\w\\.[a-z]+)$ 。这可以在多个域和应用程序之间共享会话。如果正则表达式不匹配,则不会设置域,并且会使用现有的域。如果正则表达式匹配,则第一个 grouping 将用作域。这意味着对 [role="bare"][role="bare"]https://child.example.com 的请求将域设置为 example.com 。然而,对 [role="bare"][role="bare"]http://localhost:8080/ 或 [role="bare"][role="bare"]https://192.168.1.100:8080/ 的请求会使 cookie 未设置,因此在开发中无需任何更改即可继续使用。 |
你应当只匹配有效的域名字符,因为域名会反映在响应中。这样做可以防止恶意用户实施攻击,比如 HTTP Response Splitting。
Customizing CookieSerializer
您可以使用 DefaultCookieSerializer
上的任何以下配置选项自定义如何写入会话 cookie。
-
cookieName
:要使用的 cookie 名称。默认:SESSION
。 -
useSecureCookie
:指定是否应使用安全 cookie。默认:创建时使用HttpServletRequest.isSecure()
的值。 -
cookiePath
:cookie 的路径。默认:上下文根。 -
cookieMaxAge
:指定创建会话时要设置的 cookie 的最大生存期。默认:-1
,表示在关闭浏览器时应删除 cookie。 -
jvmRoute
:指定要追加到会话 ID 中并在 cookie 中包含的后缀。用于标识应路由到哪个 JVM 来获得会话关联性。对于某些实现(即 Redis),此选项不提供性能优势。然而,它可以帮助跟踪特定用户的日志。 -
domainName
:允许指定要用于 cookie 的特定域名。这个选项易于理解,但通常需要开发环境和生产环境之间的不同配置。将domainNamePattern
视为一种替代方案。 -
domainNamePattern
:不区分大小写的模式,用于从HttpServletRequest#getServerName()
中提取域名。模式应提供一个单独的分组,该分组用于提取 cookie 域的值。如果正则表达式不匹配,则不会设置域,并且会使用现有的域。如果正则表达式匹配,则第一个 grouping 将用作域。 -
sameSite
:用于SameSite
cookie 指令的值。若要禁用SameSite
cookie 指令的序列化,可以将此值设为null
。默认:Lax
你应当只匹配有效的域名字符,因为域名会反映在响应中。这样做可以防止恶意用户实施攻击,比如 HTTP Response Splitting。
Customizing SessionRepository
实现一个自定义 <<`SessionRepository`,api-sessionrepository>> API 应该是一个相当简单的任务。将自定义实现与 <<`@EnableSpringHttpSession`,api-enablespringhttpsession>> 支持结合起来,可以让您重用现有的 Spring Session 配置设施和基础结构。然而,有几个方面值得仔细考虑。
在 HTTP 请求的生命周期中,HttpSession
通常会持久化到 SessionRepository
中两次。第一个持久化操作是为了确保会话在客户端可以访问会话 ID 时可供客户端使用,并且在提交会话后也需要写入,因为可能会对会话进行进一步修改。考虑到这一点,我们通常建议 SessionRepository
实现跟踪更改以确保只保存增量。这在高并发环境中尤为重要,在该环境中,多个请求对同一 HttpSession
进行操作,因此会引起竞争条件,导致请求相互覆盖对会话属性的更改。Spring Session 提供的所有 SessionRepository
实现都使用所描述的方法来持久化会话更改,可在您实现自定义 SessionRepository
时作为指导。
请注意,同样的建议同样适用于实现自定义 <<`ReactiveSessionRepository`,api-reactivesessionrepository>>。在这种情况下,您应该使用 <<`@EnableSpringWebSession`,api-enablespringwebsession>>。