跳到主要内容
版本:4.1.1

JDBC

Doubao 中英对照 JDBC Configurations

为你的应用添加 Spring Session JDBC​

要使用 Spring Session JDBC,你必须将 org.springframework.session:spring-session-jdbc 依赖添加到你的应用程序中。

implementation 'org.springframework.session:spring-session-jdbc'

如果你使用 Spring Boot,它会负责启用 Spring Session JDBC,更多细节请参阅 {spring-boot-ref-docs}/web.html#web.spring-session[其文档]。否则,你需要在配置类上添加 @EnableJdbcHttpSession:

@Configuration
@EnableJdbcHttpSession
public class SessionConfig {
//...
}

就是这样,你的应用程序现在应该已经配置为使用 Spring Session JDBC 了。

了解会话存储细节​

默认情况下,该实现使用 SPRING_SESSION 和 SPRING_SESSION_ATTRIBUTES 表来存储会话。请注意,当你自定义表名时,用于存储属性的表名是在提供的表名后加上 _ATTRIBUTES 后缀。如果需要进一步自定义,你可以自定义存储库使用的 SQL 查询。

例如,对于 PostgreSQL,你可以使用以下 schema 脚本:

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 属性:

@Configuration
@EnableJdbcHttpSession(tableName = "MY_TABLE_NAME")
public class SessionConfig {
//...
}

另一种方式是暴露一个 SessionRepositoryCustomizer<JdbcIndexedSessionRepository> 实现作为 bean,直接在实现中修改表名:

@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 方法。

@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 = convert_from(?, 'UTF8')::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> 实现,它们为最常见的 RDBMS 配置了优化的 SQL 查询。

将会话属性保存为 JSON​

默认情况下,Spring Session JDBC 将会话属性值保存为字节数组,该数组是对属性值进行 JDK 序列化的结果。

在本例中,我们将使用 PostgreSQL 作为 RDBMS,并使用 JSON 而非 JDK 序列化来序列化会话属性值。我们先创建 SPRING_SESSION_ATTRIBUTES 表,并为 attribute_values 列使用 jsonb 类型。

CREATE TABLE SPRING_SESSION
(
-- ...
);

-- indexes...

CREATE TABLE SPRING_SESSION_ATTRIBUTES
(
-- ...
ATTRIBUTE_BYTES JSONB NOT NULL,
-- ...
);

要自定义属性值的序列化方式,首先需要向 Spring Session JDBC 提供一个自定义 ConversionService,负责在 Object 与 byte[] 之间相互转换。为此,我们可以创建一个类型为 ConversionService、名为 springSessionConversionService 的 bean。

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。

@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 (?, ?, convert_from(?, 'UTF8')::jsonb) // <1>
""";

private static final String UPDATE_SESSION_ATTRIBUTE_QUERY = """
UPDATE %TABLE_NAME%_ATTRIBUTES
SET ATTRIBUTE_BYTES = convert_from(?, 'UTF8')::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);
};
}

}
备注

如果你的 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:

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;
}

}

自定义 Spring Session JDBC 如何使用事务​

所有 JDBC 操作都以事务方式执行。事务的传播行为设置为 REQUIRES_NEW,以避免因与现有事务相互干扰而产生意外行为(例如,在一个已参与只读事务的线程中执行保存操作)。要自定义 Spring Session JDBC 如何使用事务,你可以提供一个名为 springSessionTransactionOperations 的 TransactionOperations bean。例如,如果你想完全禁用事务,可以这样做:

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 限定 bean 来指定希望 Spring Session JDBC 使用哪个 TransactionManager bean:

@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 表达式:

@Configuration
@EnableJdbcHttpSession(cleanupCron = "0 0 * * * *") // top of every hour of every day
public class SessionConfig {

}

或者,如果你使用 Spring Boot,可以设置 spring.session.jdbc.cleanup-cron 属性:

spring.session.jdbc.cleanup-cron="0 0 * * * *"

禁用任务​

要禁用任务,必须向 @EnableJdbcHttpSession 的 cleanupCron 属性传入 Scheduled.CRON_DISABLED:

@Configuration
@EnableJdbcHttpSession(cleanupCron = Scheduled.CRON_DISABLED)
public class SessionConfig {

}

自定义按过期时间删除的查询​

你可以通过 SessionRepositoryCustomizer<JdbcIndexedSessionRepository> bean 使用 JdbcIndexedSessionRepository.setDeleteSessionsByExpiryTimeQuery 来自定义删除过期会话的查询:

@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'
""");
}

}