该版本仍在开发中,尚未被视为稳定。最新稳定版请使用Spring Session 3.5.3spring-doc.cadn.net.cn

JDBC

将春季课程JDBC添加到您的申请中

要使用春季课程 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用来存储会话的表格。 注意,当你自定义表名时,用于存储属性的表会用提供的表名命名,后缀为_属性. 如果需要进一步定制,你可以自定义仓库使用的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
);

自定义表名

要自定义数据库表的名称,可以使用tableName属性来自@EnableJdbcHttpSession注解: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 查询,您可以使用set*查询方法JdbcIndexedSessionRepository.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.

春季课程 JDBC 附带一些SessionRepositoryCustomizer<JdbcIndexedSessionRepository>为最常见的RDBMS配置优化SQL查询。spring-doc.cadn.net.cn

将会话属性保存为 JSON

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

有时将会话属性保存为不同格式(如 JSON)是有用的,这些格式可能在关系数据库管理系统中原生支持,从而在 SQL 查询中更好地兼容函数和运算符。spring-doc.cadn.net.cn

在这个例子中,我们将使用 PostgreSQL 作为我们的关系数据库管理系统,同时使用 JSON 而非 JDK 序列化会话属性值。 我们先从创建SPRING_SESSION_ATTRIBUTES带有JSONB类型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 提供 a习惯转换服务负责从对象字节[]反之亦然。 为此,我们可以创建一个 类型的豆子转换服务springSessionConversionService.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 注射对象映射器该应用默认使用。 如果你愿意,可以重新创建一个。
2 制作一份副本对象映射器所以我们只对复制件施加修改。
3 既然我们用的是 Spring Security,必须注册它的 Jackson 模块,这些模块告诉 Jackson 如何正确序列化/反序列化 Spring Security 的对象。 你可能需要对会话中其他持久化的对象做同样的处理。
4 添加JsonSerializer/JsonDeserializer我们创造了转换服务.

现在我们配置了 Spring Session JDBC 如何将我们的属性值转换为字节[]我们必须自定义插入和更新会话属性的查询。这种定制是必要的,因为 Spring Session JDBC 在 SQL 语句中将内容设置为字节,然而,拜蒂亚与 不兼容JSONB因此,我们需要编码拜蒂亚将值转为文本,然后转换为JSONB.spring-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 编码函数从拜蒂亚文本

仅此而已,你现在应该能在数据库中看到保存为 JSON 的会话属性。有一个示例可以查看整个实现并运行测试。spring-doc.cadn.net.cn

如果你用户详情实现扩展了春季安全org.springframework.security.core.userdetails.User类,重要的是你注册一个自定义的解串器。否则,Jackson会使用现有的org.springframework.security.jackson2.UserDeserializer但这不会导致预期的结果用户详情实现。 详情请参见gh-3009spring-doc.cadn.net.cn

指定一个备选方案数据来源

默认情况下,春季会话JDBC使用主数据来源应用程序中可用的豆子。不过,在某些情况下,应用可能包含多个数据来源在这种情况下,你可以告诉春季课程 JDBC数据来源通过对豆子进行限定,用@SpringSessionDataSource: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告诉春季课程JDBC,应该用那颗豆子作为数据来源.

定制 JDBC 如何使用 Spring Session 的交易

所有JDBC作均以事务方式执行。事务的传播设置为REQUIRES_NEW以避免因干扰现有事务而出现意外行为(例如,在已经参与只读事务的线程中运行保存作)。为了自定义 Spring Session JDBC 如何使用事务,你可以提供事务作豆子命名springSessionTransactionOperations. 例如,如果你想整体禁用交易,可以这样做:spring-doc.cadn.net.cn

import org.springframework.transaction.support.TransactionOperations;

@Configuration
@EnableJdbcHttpSession
public class SessionConfig {

    @Bean("springSessionTransactionOperations")
    public TransactionOperations springSessionTransactionOperations() {
        return TransactionOperations.withoutTransaction();
    }

}

如果你想要更多控制权,也可以提供事务管理器该配置被配置者使用交易模板. 默认情况下,Spring Session会尝试解决主会议事务管理器来自应用上下文的 BEAN 。在某些情况下,例如当有多个数据来源很可能会有多个事务管理器你能分辨出哪种事务管理器你想用的豆子,用 JDBC 来做条件@SpringSessionTransactionManager: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

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

你可以自定义定义清理工作运行频率的cron表达式,方法是使用cleanupCron属性@EnableJdbcHttpSession: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属性@EnableJdbcHttpSession:spring-doc.cadn.net.cn

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

}

定制“按到期时间删除”查询

你可以通过以下方式自定义删除过期会话的查询JdbcIndexedSessionRepository.setDeleteSessionsByExpiryTimeQuery通过一个SessionRepositoryCustomizer<JdbcIndexedSessionRepository>豆: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'
            """);
    }

}