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 查詢,您可以使用 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
            """;

    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 提供一個自訂 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)
        // 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 Modules,以告知 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 時,很可能會有 Multiple 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,它為批次處理應用程式提供了穩健的解決方案。

自訂清除過期工作階段的頻率

您可以自訂 cron 運算式,其定義清除作業的執行頻率,方法是使用 @EnableJdbcHttpSession 中的 cleanupCron 屬性

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

}