JDBC

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。

理解會話儲存詳情

預設情況下,實現使用 SPRING_SESSIONSPRING_SESSION_ATTRIBUTES 表來儲存會話。請注意,當您自定義表名時,用於儲存屬性的表將使用提供的表名並加上 _ATTRIBUTES 字尾命名。如果需要進一步自定義,您可以自定義倉庫使用的 SQL 查詢

由於各種資料庫供應商之間的差異,尤其是在儲存二進位制資料方面,請確保使用特定於您的資料庫的 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
);

自定義表名

要自定義資料庫表名,您可以使用 @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");
    }

}

自定義 SQL 查詢

有時,能夠自定義 Spring Session JDBC 執行的 SQL 查詢會很有用。在某些場景下,資料庫中的會話或其屬性可能會發生併發修改,例如,一個請求可能嘗試插入一個已存在的屬性,導致重複鍵異常。因此,您可以應用特定於 RDBMS 的查詢來處理這些場景。要自定義 Spring Session JDBC 對資料庫執行的 SQL 查詢,您可以使用 JdbcIndexedSessionRepositoryset*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
            """;

    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 查詢。

將會話屬性儲存為 JSON

預設情況下,Spring Session JDBC 將會話屬性值儲存為位元組陣列,該陣列是屬性值經過 JDK 序列化後的結果。

有時將會在話屬性儲存為不同的格式(例如 JSON)會很有用,因為 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 提供一個負責將 Object 轉換為 byte[],反之亦然的自定義 ConversionService。為此,我們可以建立一個名為 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)
        // 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 語句中將內容設定為位元組,然而 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)
            """;

    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

就這樣,您現在應該能夠看到會話屬性以 JSON 格式儲存在資料庫中。這裡有一個可用的示例,您可以在其中檢視完整的實現並執行測試。

如果您的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

  • 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 Session JDBC 它應該使用該 bean 作為 DataSource

自定義 Spring Session JDBC 使用事務的方式

所有 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 時,很可能存在多個 TransactionManager,您可以透過使用 @SpringSessionTransactionManager 限定它來告訴 Spring Session JDBC 您想要使用哪個 TransactionManager bean。

  • 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,它為批處理應用程式提供了強大的解決方案。

自定義過期會話清理頻率

您可以使用 @EnableJdbcHttpSession 中的 cleanupCron 屬性來自定義定義清理作業執行頻率的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 * * * *"

停用作業

要停用作業,您必須將 Scheduled.CRON_DISABLED 傳遞給 @EnableJdbcHttpSession 中的 cleanupCron 屬性

  • Java

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

}

自定義按過期時間刪除查詢

您可以透過 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'
            """);
    }

}