JDBC
Spring Session JDBC 是一个使用 JDBC 作为数据存储的会话管理模块。
-
我想自定义表名
-
我想将会话属性保存为 JSON而不是字节数组
-
我想自定义过期会话清理作业
将 Spring Session JDBC 添加到您的应用程序
要使用 Spring Session JDBC,您必须将 org.springframework.session:spring-session-jdbc
依赖项添加到您的应用程序中。
- Gradle
- Maven
implementation 'org.springframework.session:spring-session-jdbc'
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-jdbc</artifactId>
</dependency>
如果你使用的是 Spring Boot,它将负责启用 Spring Session JDBC,请参阅其文档以获取更多详细信息。否则,你需要在配置类中添加 @EnableJdbcHttpSession
:
- Java
@Configuration
@EnableJdbcHttpSession
public class SessionConfig {
//...
}
就这样,您的应用程序现在应该已经配置为使用 Spring Session JDBC。
了解会话存储详情
默认情况下,实现使用 SPRING_SESSION
和 SPRING_SESSION_ATTRIBUTES
表来存储会话。请注意,当您自定义表名时,用于存储属性的表将使用提供的表名加上 _ATTRIBUTES
后缀来命名。如果需要进一步的自定义,您可以自定义仓库使用的 SQL 查询。
由于各个数据库供应商之间存在差异,特别是在存储二进制数据时,请确保使用特定于您的数据库的 SQL 脚本。大多数主要数据库供应商的脚本被打包为 org/springframework/session/jdbc/schema-*.sql
,其中 *
是目标数据库类型。
例如,对于 PostgreSQL,你可以使用以下模式脚本:
CREATE TABLE SPRING_SESSION (
PRIMARY_ID CHAR(36) NOT NULL,
SESSION_ID CHAR(36) NOT NULL,
CREATION_TIME BIGINT NOT NULL,
LAST_ACCESS_TIME BIGINT NOT NULL,
MAX_INACTIVE_INTERVAL INT NOT NULL,
EXPIRY_TIME BIGINT NOT NULL,
PRINCIPAL_NAME VARCHAR(100),
CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (PRIMARY_ID)
);
CREATE UNIQUE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (SESSION_ID);
CREATE INDEX SPRING_SESSION_IX2 ON SPRING_SESSION (EXPIRY_TIME);
CREATE INDEX SPRING_SESSION_IX3 ON SPRING_SESSION (PRINCIPAL_NAME);
CREATE TABLE SPRING_SESSION_ATTRIBUTES (
SESSION_PRIMARY_ID CHAR(36) NOT NULL,
ATTRIBUTE_NAME VARCHAR(200) NOT NULL,
ATTRIBUTE_BYTES BYTEA NOT NULL,
CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_PRIMARY_ID, ATTRIBUTE_NAME),
CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_PRIMARY_ID) REFERENCES SPRING_SESSION(PRIMARY_ID) ON DELETE CASCADE
);
自定义表名
要自定义数据库表名,可以使用 @EnableJdbcHttpSession
注解中的 tableName
属性:
- Java
@Configuration
@EnableJdbcHttpSession(tableName = "MY_TABLE_NAME")
public class SessionConfig {
//...
}
另一种选择是暴露一个 SessionRepositoryCustomizer<JdbcIndexedSessionRepository>
的实现作为 bean,以便在实现中直接更改表:
- Java
@Configuration
@EnableJdbcHttpSession
public class SessionConfig {
@Bean
public TableNameCustomizer tableNameCustomizer() {
return new TableNameCustomizer();
}
}
public class TableNameCustomizer
implements SessionRepositoryCustomizer<JdbcIndexedSessionRepository> {
@Override
public void customize(JdbcIndexedSessionRepository sessionRepository) {
sessionRepository.setTableName("MY_TABLE_NAME");
}
}
自定义 SQL 查询
有时,能够自定义 Spring Session JDBC 执行的 SQL 查询是非常有用的。在某些场景下,可能会对数据库中的会话或其属性进行并发修改,例如,一个请求可能希望插入一个已经存在的属性,从而导致重复键异常。因此,你可以应用特定于 RDBMS 的查询来处理此类场景。要自定义 Spring Session JDBC 对数据库执行的 SQL 查询,可以使用 JdbcIndexedSessionRepository
中的 set*Query
方法。
- Java
@Configuration
@EnableJdbcHttpSession
public class SessionConfig {
@Bean
public QueryCustomizer tableNameCustomizer() {
return new QueryCustomizer();
}
}
public class QueryCustomizer
implements SessionRepositoryCustomizer<JdbcIndexedSessionRepository> {
private static final String CREATE_SESSION_ATTRIBUTE_QUERY = """
INSERT INTO %TABLE_NAME%_ATTRIBUTES (SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES) // <1>
VALUES (?, ?, ?)
ON CONFLICT (SESSION_PRIMARY_ID, ATTRIBUTE_NAME)
DO NOTHING
""";
private static final String UPDATE_SESSION_ATTRIBUTE_QUERY = """
UPDATE %TABLE_NAME%_ATTRIBUTES
SET ATTRIBUTE_BYTES = encode(?, 'escape')::jsonb
WHERE SESSION_PRIMARY_ID = ?
AND ATTRIBUTE_NAME = ?
""";
@Override
public void customize(JdbcIndexedSessionRepository sessionRepository) {
sessionRepository.setCreateSessionAttributeQuery(CREATE_SESSION_ATTRIBUTE_QUERY);
sessionRepository.setUpdateSessionAttributeQuery(UPDATE_SESSION_ATTRIBUTE_QUERY);
}
}
查询中的
%TABLE_NAME%
占位符将被JdbcIndexedSessionRepository
使用的配置表名替换。
Spring Session JDBC 附带了几个 SessionRepositoryCustomizer<JdbcIndexedSessionRepository>
的实现,这些实现为最常见的关系数据库管理系统配置了优化的 SQL 查询。
将会话属性保存为 JSON
默认情况下,Spring Session JDBC 会将会话属性值保存为字节数组,该数组是属性值通过 JDK 序列化得到的结果。
有时将会话属性保存为不同的格式(如 JSON)会很有用,这可能会在关系数据库管理系统(RDBMS)中获得原生支持,从而在 SQL 查询中实现更好的函数和操作符兼容性。
对于这个示例,我们将使用 PostgreSQL 作为我们的关系数据库管理系统(RDBMS),并将使用 JSON 而不是 JDK 序列化来序列化会话属性值。让我们首先创建 SPRING_SESSION_ATTRIBUTES
表,并将 attribute_values
列设置为 jsonb
类型。
- SQL
CREATE TABLE SPRING_SESSION
(
-- ...
);
-- indexes...
CREATE TABLE SPRING_SESSION_ATTRIBUTES
(
-- ...
ATTRIBUTE_BYTES JSONB NOT NULL,
-- ...
);
要自定义属性值的序列化方式,首先我们需要为 Spring Session JDBC 提供一个自定义 ConversionService,该服务负责将 Object
转换为 byte[]
及其反向操作。为此,我们可以创建一个名为 springSessionConversionService
的类型为 ConversionService
的 bean。
- Java
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.core.serializer.support.DeserializingConverter;
import org.springframework.core.serializer.support.SerializingConverter;
@Configuration
@EnableJdbcHttpSession
public class SessionConfig implements BeanClassLoaderAware {
private ClassLoader classLoader;
@Bean("springSessionConversionService")
public GenericConversionService springSessionConversionService(ObjectMapper objectMapper) { 1
ObjectMapper copy = objectMapper.copy(); 2
// Register Spring Security Jackson Modules
copy.registerModules(SecurityJackson2Modules.getModules(this.classLoader)); 3
// Activate default typing explicitly if not using Spring Security
// copy.activateDefaultTyping(copy.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
GenericConversionService converter = new GenericConversionService();
converter.addConverter(Object.class, byte[].class, new SerializingConverter(new JsonSerializer(copy))); 4
converter.addConverter(byte[].class, Object.class, new DeserializingConverter(new JsonDeserializer(copy))); 4
return converter;
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
}
static class JsonSerializer implements Serializer<Object> {
private final ObjectMapper objectMapper;
JsonSerializer(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public void serialize(Object object, OutputStream outputStream) throws IOException {
this.objectMapper.writeValue(outputStream, object);
}
}
static class JsonDeserializer implements Deserializer<Object> {
private final ObjectMapper objectMapper;
JsonDeserializer(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public Object deserialize(InputStream inputStream) throws IOException {
return this.objectMapper.readValue(inputStream, Object.class);
}
}
}
注入应用程序默认使用的
ObjectMapper
。如果你愿意,也可以创建一个新的。创建该
ObjectMapper
的副本,以便我们仅对副本进行更改。由于我们使用了 Spring Security,因此必须注册其 Jackson 模块,以告诉 Jackson 如何正确地序列化/反序列化 Spring Security 的对象。对于其他持久化在会话中的对象,你可能也需要这样做。
将我们创建的
JsonSerializer
/JsonDeserializer
添加到ConversionService
中。
现在我们已经配置了 Spring Session JDBC 如何将我们的属性值转换为 byte[]
,我们必须自定义插入和更新会话属性的查询。这种自定义是必要的,因为 Spring Session JDBC 在 SQL 语句中将内容设置为字节,但是 bytea
与 jsonb
不兼容,因此我们需要将 bytea
值编码为文本,然后再将其转换为 jsonb
。
- Java
@Configuration
@EnableJdbcHttpSession
public class SessionConfig {
private static final String CREATE_SESSION_ATTRIBUTE_QUERY = """
INSERT INTO %TABLE_NAME%_ATTRIBUTES (SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES)
VALUES (?, ?, encode(?, 'escape')::jsonb) // <1>
""";
private static final String UPDATE_SESSION_ATTRIBUTE_QUERY = """
UPDATE %TABLE_NAME%_ATTRIBUTES
SET ATTRIBUTE_BYTES = encode(?, 'escape')::jsonb
WHERE SESSION_PRIMARY_ID = ?
AND ATTRIBUTE_NAME = ?
""";
@Bean
SessionRepositoryCustomizer<JdbcIndexedSessionRepository> customizer() {
return (sessionRepository) -> {
sessionRepository.setCreateSessionAttributeQuery(CREATE_SESSION_ATTRIBUTE_QUERY);
sessionRepository.setUpdateSessionAttributeQuery(UPDATE_SESSION_ATTRIBUTE_QUERY);
};
}
}
使用 PostgreSQL encode 函数将
bytea
转换为text
就这样,现在你应该能够在数据库中看到保存为 JSON 格式的会话属性。有一个示例可用,你可以在其中查看整个实现并运行测试。
如果你的UserDetails 实现继承了 Spring Security 的 org.springframework.security.core.userdetails.User
类,那么为它注册一个自定义反序列化器是很重要的。否则,Jackson 将使用现有的 org.springframework.security.jackson2.UserDeserializer
,这将不会产生预期的 UserDetails
实现。更多详情请参阅 gh-3009。
指定替代的 DataSource
默认情况下,Spring Session JDBC 使用应用程序中可用的主要 DataSource
bean。但是,在某些情况下,应用程序可能有多个 DataSource
bean,在这种情况下,你可以通过使用 @SpringSessionDataSource
来限定 bean,告诉 Spring Session JDBC 使用哪个 DataSource
:
- Java
import org.springframework.session.jdbc.config.annotation.SpringSessionDataSource;
@Configuration
@EnableJdbcHttpSession
public class SessionConfig {
@Bean
public DataSource dataSourceOne() {
// create and configure datasource
return dataSourceOne;
}
@Bean
@SpringSessionDataSource 1
public DataSource dataSourceTwo() {
// create and configure datasource
return dataSourceTwo;
}
}
我们用
@SpringSessionDataSource
注解dataSourceTwo
bean,以告诉 Spring Session JDBC 应该使用该 bean 作为DataSource
。
自定义 Spring Session JDBC 如何使用事务
所有JDBC操作都以事务方式执行。事务的传播行为设置为 REQUIRES_NEW
,以避免由于与现有事务(例如,在已经参与只读事务的线程中运行保存操作)发生干扰而导致的意外行为。要自定义Spring Session JDBC如何使用事务,你可以提供一个名为 springSessionTransactionOperations
的 TransactionOperations
bean。例如,如果你想完全禁用事务,可以这样做:
- Java
import org.springframework.transaction.support.TransactionOperations;
@Configuration
@EnableJdbcHttpSession
public class SessionConfig {
@Bean("springSessionTransactionOperations")
public TransactionOperations springSessionTransactionOperations() {
return TransactionOperations.withoutTransaction();
}
}
如果你需要更多的控制,你也可以提供配置的 TransactionTemplate
所使用的 TransactionManager
。默认情况下,Spring Session 会尝试从应用程序上下文中解析主 TransactionManager
bean。在某些场景下,例如存在多个 DataSource
时,很可能也会有多个 TransactionManager
,你可以通过使用 @SpringSessionTransactionManager
来指定你想与 Spring Session JDBC 一起使用的 TransactionManager
bean:
- Java
@Configuration
@EnableJdbcHttpSession
public class SessionConfig {
@Bean
@SpringSessionTransactionManager
public TransactionManager transactionManager1() {
return new MyTransactionManager();
}
@Bean
public TransactionManager transactionManager2() {
return otherTransactionManager;
}
}
自定义过期会话清理作业
为了避免数据库因过期会话而负担过重,Spring Session JDBC 每分钟执行一次清理作业,删除过期的会话(及其属性)。有多种原因可能需要自定义清理作业,让我们在接下来的部分中看看最常见的几种。然而,默认作业的自定义是有限的,这是有意为之的,因为 Spring Session 并不旨在提供强大的批处理功能,有很多框架或库在这方面做得更好。因此,如果您需要更多的自定义功能,请考虑禁用默认作业并提供您自己的作业。一个很好的替代方案是使用 Spring Batch,它为批处理应用程序提供了强大的解决方案。
自定义清理过期会话的频率
你可以通过在 @EnableJdbcHttpSession
中使用 cleanupCron
属性来自定义定义清理作业运行频率的 cron 表达式:
- Java
@Configuration
@EnableJdbcHttpSession(cleanupCron = "0 0 * * * *") // top of every hour of every day
public class SessionConfig {
}
或者,如果你使用的是 Spring Boot,请设置 spring.session.jdbc.cleanup-cron
属性:
- application.properties
spring.session.jdbc.cleanup-cron="0 0 * * * *"
禁用作业
要禁用该作业,您必须将 Scheduled.CRON_DISABLED
传递给 @EnableJdbcHttpSession
中的 cleanupCron
属性:
- Java
@Configuration
@EnableJdbcHttpSession(cleanupCron = Scheduled.CRON_DISABLED)
public class SessionConfig {
}
自定义删除过期时间查询
你可以通过 SessionRepositoryCustomizer<JdbcIndexedSessionRepository>
bean 使用 JdbcIndexedSessionRepository.setDeleteSessionsByExpiryTimeQuery
来自定义删除过期会话的查询:
- Java
@Configuration
@EnableJdbcHttpSession
public class SessionConfig {
@Bean
public SessionRepositoryCustomizer<JdbcIndexedSessionRepository> customizer() {
return (sessionRepository) -> sessionRepository.setDeleteSessionsByExpiryTimeQuery("""
DELETE FROM %TABLE_NAME%
WHERE EXPIRY_TIME < ?
AND OTHER_COLUMN = 'value'
""");
}
}