对于最新稳定版本,请使用 Spring Session 4.0.2spring-doc.cadn.net.cn

JDBC

Spring Session JDBC 是一个模块,能够使用JDBC作为数据存储来实现会话管理。spring-doc.cadn.net.cn

将 Spring Session JDBC 添加到您的应用程序

要使用Spring Session JDBC,您必须将org.springframework.session:spring-session-jdbc依赖项添加到您的应用程序中spring-doc.cadn.net.cn

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:spring-doc.cadn.net.cn

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

这就是全部配置了,您的应用程序现在应该已经配置为使用 Spring Session JDBC。spring-doc.cadn.net.cn

了解会话存储详情

默认情况下,实现使用SPRING_SESSIONSPRING_SESSION_ATTRIBUTES表来存储会话。 请注意,当您自定义表名时,用于存储属性的表名称由提供的表名后缀_ATTRIBUTES得到。 如果需要进一步的自定义,则可以自定义仓库使用的SQL查询spring-doc.cadn.net.cn

由于各个数据库提供商之间存在差异,尤其是在存储二进制数据时,请确保使用针对您所用数据库的特定 SQL 脚本。 大多数主要数据库提供商的脚本被打包为org/springframework/session/jdbc/schema-*.sql,其中*是指定的目标数据库类型。spring-doc.cadn.net.cn

例如,使用PostgreSQL时,您可以使用以下模式脚本:spring-doc.cadn.net.cn

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属性:spring-doc.cadn.net.cn

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

另一种替代方案是暴露一个实现SessionRepositoryCustomizer<JdbcIndexedSessionRepository>的bean,以便在实现中直接更改表:spring-doc.cadn.net.cn

@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方法。spring-doc.cadn.net.cn

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

}
1 在查询中的%TABLE_NAME%占位符将会被JdbcIndexedSessionRepository所配置使用的表名替换。

Spring Session JDBC 随附了几种实现 SessionRepositoryCustomizer<JdbcIndexedSessionRepository>,这些实现配置了针对最常见的关系数据库管理系统(RDBMS)优化的 SQL 查询。spring-doc.cadn.net.cn

将会话属性保存为 JSON

默认情况下,Spring Session JDBC 将会话属性值保存为字节数组,该数组是来自 JDK 序列化的属性值的结果。spring-doc.cadn.net.cn

有时,将会话属性保存在不同的格式(例如JSON)中是有用的,因为在RDBMS中可能具有原生支持,这使得在SQL查询中的函数和操作符兼容性更好。spring-doc.cadn.net.cn

对于这个示例,我们将使用PostgreSQL作为我们的关系型数据库管理系统(RDBMS),并且将会话属性值序列化为JSON而非JDK序列化。 让我们从创建一个带有jsonb类型的SPRING_SESSION_ATTRIBUTES表和attribute_values列开始。spring-doc.cadn.net.cn

CREATE TABLE SPRING_SESSION
(
    -- ...
);

-- indexes...

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

要自定义属性值的序列化方式,首先我们需要为 Spring Session JDBC 提供一个 自定义的 ConversionService,负责在 Objectbyte[] 之间进行双向转换。 为此,我们可以创建一个类型为 ConversionService、名称为 springSessionConversionService 的 Bean。spring-doc.cadn.net.cn

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

    }

}
1 注入应用中默认使用的ObjectMapper。 如果你偏好,也可以创建一个新的。
2 创建一个 ObjectMapper 的副本,以便我们只对副本进行更改。
3 由于我们使用了Spring Security,我们必须注册其Jackson模块,以告诉Jackson如何正确序列化/反序列化Spring Security的对象。 您可能还需要为其他在会话中持久化的对象执行相同的操作。
4 将我们创建的JsonSerializer/JsonDeserializer添加到ConversionService中。

现在我们已经配置了如何将Spring Session JDBC转换为我们的属性值为byte[],接下来我们必须自定义插入和更新会话属性的查询。 自定义是必要的,因为Spring Session JDBC在SQL语句中设置内容为字节形式,然而bytea不兼容jsonb,因此我们需要将bytea值编码为文本,然后将其转换为jsonbspring-doc.cadn.net.cn

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

}
1 使用 PostgreSQL encode函数 将从bytea转换为text

And that’s it, you should now be able to see the session attributes saved as JSON in the database. There is a sample available where you can see the whole implementation and run the tests.spring-doc.cadn.net.cn

如果您的 UserDetails 实现类 扩展了 Spring Security 的 org.springframework.security.core.userdetails.User 类,那么为其注册自定义反序列化器非常重要。 否则,Jackson 将使用现有的 org.springframework.security.jackson2.UserDeserializer,这不会生成预期的 UserDetails 实现。有关更多详细信息,请参阅 gh-3009spring-doc.cadn.net.cn

指定替代方案DataSource

默认情况下,Spring Session JDBC 使用应用程序中可用的主 DataSource 颗粒。 然而,在某些场景下,一个应用可能有多个 DataSource 颗粒。在这种场景下,您可以使用 @SpringSessionDataSourceDataSource 颗粒进行限定,以告诉 Spring Session JDBC 使用哪个 DataSource 颗粒:spring-doc.cadn.net.cn

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

}
1 我们使用@SpringSessionDataSource注解 dataSourceTwo 容器中的bean,告诉 Spring Session JDBC 应该使用该bean作为DataSource

自定义 Spring Session JDBC 使用事务的方式

所有JDBC操作都是在事务管理下进行的。 事务使用传播设置为REQUIRES_NEW,以避免现有事务(例如,在已参与只读事务的线程中执行保存操作)带来的意外行为干扰。 为了自定义Spring Session JDBC如何使用事务,可以提供一个名为springSessionTransactionOperationsTransactionOperations bean。 例如,如果你想整体禁用事务,你可以这样做:spring-doc.cadn.net.cn

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:spring-doc.cadn.net.cn

@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,它为批处理应用程序提供了强大的解决方案。spring-doc.cadn.net.cn

自定义过期会话的清理频率

您可以通过在@EnableJdbcHttpSession中使用cleanupCron属性来自定义定义清理作业运行频率的cron表达式spring-doc.cadn.net.cn

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

}

或者如果你使用的是Spring Boot,请设置spring.session.jdbc.cleanup-cron属性:spring-doc.cadn.net.cn

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

禁用任务

要禁用该作业,您必须将Scheduled.CRON_DISABLED传递给cleanupCron属性中的@EnableJdbcHttpSessionspring-doc.cadn.net.cn

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

}

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

您可以使用JdbcIndexedSessionRepository.setDeleteSessionsByExpiryTimeQuerySessionRepositoryCustomizer<JdbcIndexedSessionRepository>的bean来自定义删除过期会话的查询。spring-doc.cadn.net.cn

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

}