对于最新的稳定版本,请使用 Spring Session 3.3.1

对于最新的稳定版本,请使用 Spring Session 3.3.1

Spring Session JDBC 是一个使用 JDBC 作为数据存储启用会话管理的模块。

将 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。

了解会话存储详细信息

默认情况下,实现使用 和 表来存储会话。 请注意,自定义表名时,用于存储属性的表使用提供的表名(后缀为 )进行命名。 如果需要进一步的自定义,您可以自定义存储库使用的 SQL 查询SPRING_SESSIONSPRING_SESSION_ATTRIBUTES_ATTRIBUTES

由于各种数据库供应商之间的差异,尤其是在存储二进制数据时,请确保使用特定于数据库的 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
);

自定义表名

要自定义数据库表名称,可以使用注释中的属性:tableName@EnableJdbcHttpSession

  • Java

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

另一种选择是公开一个 as a bean 的实现,以直接在实现中更改表:SessionRepositoryCustomizer<JdbcIndexedSessionRepository>

  • 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 查询,可以使用 中的方法。set*QueryJdbcIndexedSessionRepository

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

}
1 查询中的占位符将替换为 正在使用的已配置表名。%TABLE_NAME%JdbcIndexedSessionRepository

Spring Session JDBC 附带了一些实现,这些实现为最常见的 RDBMS 配置了优化的 SQL 查询。SessionRepositoryCustomizer<JdbcIndexedSessionRepository>

1 查询中的占位符将替换为 正在使用的已配置表名。%TABLE_NAME%JdbcIndexedSessionRepository

Spring Session JDBC 附带了一些实现,这些实现为最常见的 RDBMS 配置了优化的 SQL 查询。SessionRepositoryCustomizer<JdbcIndexedSessionRepository>

将会话属性另存为 JSON

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

有时,以不同的格式(如 JSON)保存会话属性很有用,这些格式可能在 RDBMS 中具有本机支持,从而在 SQL 查询中实现更好的函数和运算符兼容性。

在此示例中,我们将使用 PostgreSQL 作为 RDBMS,并使用 JSON 而不是 JDK 序列化序列化会话属性值。 让我们首先创建具有列类型的表。SPRING_SESSION_ATTRIBUTESjsonbattribute_values

  • SQL

CREATE TABLE SPRING_SESSION
(
    -- ...
);

-- indexes...

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

要自定义属性值的序列化方式,首先我们需要向 Spring Session JDBC 提供一个自定义 ConversionService,负责从 到 转换,反之亦然。 为此,我们可以创建一个名为 的 Bean 类型。Objectbyte[]ConversionServicespringSessionConversionService

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

    }

}
1 注入应用程序中默认使用的 that。 如果您愿意,可以创建一个新的。ObjectMapper
2 创建一个副本,以便我们只将更改应用于副本。ObjectMapper
3 由于我们使用的是 Spring Security,因此我们必须注册其 Jackson 模块,该模块告诉 Jackson 如何正确序列化/反序列化 Spring Security 的对象。 您可能需要对会话中保留的其他对象执行相同的操作。
4 将我们创建的 / 添加到 .JsonSerializerJsonDeserializerConversionService

现在,我们已经配置了Spring Session JDBC如何将我们的属性值转换为 ,我们必须自定义插入和更新会话属性的查询。 自定义是必要的,因为Spring Session JDBC在SQL语句中将内容设置为字节,但是,与 不兼容,因此我们需要将值编码为文本,然后将其转换为。byte[]byteajsonbbyteajsonb

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

}
1 使用 PostgreSQL 编码函数将byteatext

就是这样,您现在应该能够看到在数据库中保存为 JSON 的会话属性。 有一个示例可用,您可以在其中查看整个实现并运行测试。

如果您的 UserDetails 实现扩展了 Spring Security 的类,那么为它注册自定义反序列化程序非常重要。 否则,Jackson 将使用现有的,这不会导致预期的实现。有关详细信息,请参阅 gh-3009org.springframework.security.core.userdetails.Userorg.springframework.security.jackson2.UserDeserializerUserDetails

1 注入应用程序中默认使用的 that。 如果您愿意,可以创建一个新的。ObjectMapper
2 创建一个副本,以便我们只将更改应用于副本。ObjectMapper
3 由于我们使用的是 Spring Security,因此我们必须注册其 Jackson 模块,该模块告诉 Jackson 如何正确序列化/反序列化 Spring Security 的对象。 您可能需要对会话中保留的其他对象执行相同的操作。
4 将我们创建的 / 添加到 .JsonSerializerJsonDeserializerConversionService
1 使用 PostgreSQL 编码函数将byteatext

如果您的 UserDetails 实现扩展了 Spring Security 的类,那么为它注册自定义反序列化程序非常重要。 否则,Jackson 将使用现有的,这不会导致预期的实现。有关详细信息,请参阅 gh-3009org.springframework.security.core.userdetails.Userorg.springframework.security.jackson2.UserDeserializerUserDetails

指定备选方案DataSource

缺省情况下,Spring Session JDBC 使用应用程序中可用的主 Bean。 但是,在某些情况下,应用程序可能具有多个 s bean,在这种情况下,您可以通过用以下条件限定 bean 来告诉 Spring Session JDBC 要使用哪个:DataSourceDataSourceDataSource@SpringSessionDataSource

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

}
1 我们用注释 bean 告诉 Spring Session JDBC 它应该使用该 bean 作为 .dataSourceTwo@SpringSessionDataSourceDataSource
1 我们用注释 bean 告诉 Spring Session JDBC 它应该使用该 bean 作为 .dataSourceTwo@SpringSessionDataSourceDataSource

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

所有 JDBC 操作都以事务方式执行。 在执行事务时,传播设置为 以避免由于干扰现有事务而导致的意外行为(例如,在已参与只读事务的线程中运行保存操作)。 要自定义 Spring Session JDBC 使用事务的方式,您可以提供一个名为 的 Bean 。 例如,如果要从整体上禁用事务,则可以执行以下操作:REQUIRES_NEWTransactionOperationsspringSessionTransactionOperations

  • Java

import org.springframework.transaction.support.TransactionOperations;

@Configuration
@EnableJdbcHttpSession
public class SessionConfig {

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

}

如果需要更多控制,还可以提供已配置的 . 默认情况下,Spring Session 将尝试从应用程序上下文解析主 Bean。 在某些情况下,例如当有多个 s 时,很可能会有多个 s,您可以通过用以下条件限定它来判断要与 Spring Session JDBC 一起使用的 bean:TransactionManagerTransactionTemplateTransactionManagerDataSourceTransactionManagerTransactionManager@SpringSessionTransactionManager

  • 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,它为批处理应用程序提供了强大的解决方案。

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

您可以使用以下属性自定义定义清理作业运行频率的 cron 表达式cleanupCron@EnableJdbcHttpSession

  • 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_DISABLEDcleanupCron@EnableJdbcHttpSession

  • Java

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

}

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

您可以使用 via a bean 来自定义删除过期会话的查询:JdbcIndexedSessionRepository.setDeleteSessionsByExpiryTimeQuerySessionRepositoryCustomizer<JdbcIndexedSessionRepository>

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

}