JDBC
Spring Session JDBC 是一個模組,它使用 JDBC 作為資料儲存來實現會話管理。
-
我想自定義表名
-
我想將會話屬性儲存為 JSON 而不是位元組陣列
-
我想為 Spring Session JDBC 使用不同的
DataSource
-
我想自定義過期會話清理作業
將 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_SESSION
和 SPRING_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 提供了一些 |
將會話屬性儲存為 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
。為此,我們可以建立一個名為 springSessionConversionService
的 ConversionService
型別的 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 語句中將內容設定為位元組,然而 bytea
與 jsonb
不相容,因此我們需要將 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 格式儲存在資料庫中。這裡有一個可用的示例,您可以在其中檢視完整的實現並執行測試。
如果您的 |
指定備用 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 使用事務的方式,您可以提供一個名為 springSessionTransactionOperations
的 TransactionOperations
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'
""");
}
}