JDBC

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

Adding Spring Session JDBC To Your Application

若要使用 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,请参阅其 {spring-boot-ref-docs}/web.html#web.spring-session[文档] 了解更多详情。否则,您需要将 @EnableJdbcHttpSession 添加到配置类:

  • Java

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

就是这些,您的应用程序现在应该配置为使用 Spring Session JDBC。

Understanding the Session Storage Details

默认情况下,该实现使用 SPRING_SESSIONSPRING_SESSION_ATTRIBUTES 表存储会话。请注意,当您 customize the table name 时,用于存储属性的表通过使用带有 _ATTRIBUTES 后缀的表名命名。如果需要进一步的自定义,您可以 customize the SQL queries used by the repository

由于各种数据库供应商之间存在差异,尤其是在存储二进制数据时,请确保使用特定于您的数据库的 SQL 脚本。大多数主要数据库供应商的脚本打包为 org/springframework/session/jdbc/schema-.sql, where 是目标数据库类型。

例如,对于 PostgreSQL,您可以使用以下架构脚本:

Unresolved include directive in modules/ROOT/pages/configuration/jdbc.adoc - include::example$session-jdbc-main-resources-dir/org/springframework/session/jdbc/schema-postgresql.sql[]

Customizing the Table Name

要自定义数据库表名,您可以使用 @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");
    }

}

Customizing the SQL Queries

有时,能够自定义 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
            """;

    @Override
    public void customize(JdbcIndexedSessionRepository sessionRepository) {
        sessionRepository.setCreateSessionAttributeQuery(CREATE_SESSION_ATTRIBUTE_QUERY);
    }

}
1 查询中的 %TABLE_NAME% 占位符将被 JdbcIndexedSessionRepository 使用的已配置表名替代。

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

Saving Session Attributes as 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 提供一个 {spring-framework-ref-docs}/core/validation/convert.html#core-convert-ConversionService-API[自定义 ConversionService],负责将 Object 转换为 byte[],反之亦然。为此,我们可以创建一个名为 springSessionConversionServiceConversionService 类型 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
        copy.registerModules(SecurityJackson2Modules.getModules(this.classLoader)); 3
        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 安全,我们必须注册其 Jackson 模块,该模块告诉 Jackson 如何正确序列化/反序列化 Spring 安全的对象。您可能需要对会话中持久保存的其他对象执行相同的操作。
4 将我们创建的 JsonSerializer/JsonDeserializer 添加到 ConversionService

在我们配置了 Spring Session JDBC 如何将我们的属性值转换为 byte[] 后,我们必须自定义插入会话属性的查询。自定义是必要的,因为 Spring Session JDBC 在 SQL 语句中将内容设置为字节,但 byteajsonb 不兼容,因此我们需要将 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
            """;

    @Bean
    SessionRepositoryCustomizer<JdbcIndexedSessionRepository> customizer() {
        return (sessionRepository) -> sessionRepository.setCreateSessionAttributeQuery(CREATE_SESSION_ATTRIBUTE_QUERY);
    }

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

就是这样,您现在应该能够在数据库中将会话属性另存为 JSON。有一个 sample available,您可以在其中看到整个实现并运行测试。

Specifying an alternative DataSource

默认情况下,Spring Session JDBC 使用可用于应用程序中的主 DataSource bean。但是,在某些情况下,应用程序可能有多个 DataSource`s beans, in such scenarios you can tell Spring Session JDBC which `DataSource,可以使用带有 @SpringSessionDataSource 的 bean 限定符来使用:

  • 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 我们使用 @SpringSessionDataSource 注解 dataSourceTwo bean,以告诉 Spring 会话 JDBC 它应该将该 bean 用作 DataSource

Customizing How Spring Session JDBC Uses Transactions

所有 JDBC 操作以事务方式执行。使用传播设置为 REQUIRES_NEW 执行事务,以避免由于与现有事务干扰而导致意外行为(例如,在已参与只读事务的线程中运行保存操作)。要自定义 Spring Session JDBC 使用事务的方式,您可以提供一个名为 springSessionTransactionOperationsTransactionOperations 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`s, it is very likely that there will be multiple `TransactionManager`s, you can tell which `TransactionManager bean 可以通过使用限定符 @SpringSessionTransactionManager 与 Spring Session JDBC 配合使用:

  • Java

@Configuration
@EnableJdbcHttpSession
public class SessionConfig {

    @Bean
    @SpringSessionTransactionManager
    public TransactionManager transactionManager1() {
        return new MyTransactionManager();
    }

    @Bean
    public TransactionManager transactionManager2() {
        return otherTransactionManager;
    }

}

Customizing the Expired Sessions Clean-Up Job

为了避免使用过期会话重载数据库,Spring Session JDBC 会每分钟执行一次清理作业,删除过期的会话(及其属性)。您可能希望自定义清理作业有多种原因,让我们在以下各节中了解最常见的原因。但是,对默认作业进行的自定义是有限的,这是有意的,Spring Session 不旨在提供健壮的批处理,因为有许多框架或类库做的更好。因此,如果您希望具备更多自定义功能,请考虑 disabling the default job 并提供您自己的功能。一个不错的选择是使用 Spring Batch,它为批处理应用程序提供了健壮的解决方案。

Customizing How Often Expired Sessions Are Cleaned Up

您可以使用 @EnableJdbcHttpSession 中的 cleanupCron 属性自定义定义清理作业运行频率的 {spring-framework-ref-docs}/integration/scheduling.html#scheduling-cron-expression[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 * * * *"

Disabling the Job

要禁用作业,你必须将 Scheduled.CRON_DISABLED 传递到 @EnableJdbcHttpSession 中的 cleanupCron 属性:

  • Java

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

}

Customizing the Delete By Expiry Time Query

你可以通过 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'
            """);
    }

}