此版本仍在开发中,尚未被视为稳定版本。对于最新的稳定版本,请使用 Spring Session 3.4.2! |
JDBC
Spring Session JDBC 是一个使用 JDBC 作为数据存储实现会话管理的模块。
-
我想知道 JDBC 架构是如何定义的
-
我想自定义表名
-
我想将会话属性保存为 JSON 而不是字节数组
-
我想使用不同的
DataSource
针对 Spring Session 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_SESSION
和SPRING_SESSION_ATTRIBUTES
tables 来存储会话。
请注意,在自定义表名称时,用于存储属性的表使用提供的表名称命名,该表名称后缀为_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
);
自定义表名
要自定义数据库表名称,您可以使用tableName
属性@EnableJdbcHttpSession
注解:
-
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 查询,可以使用set*Query
methods 从JdbcIndexedSessionRepository
.
-
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% placeholder 将替换为JdbcIndexedSessionRepository . |
Spring Session JDBC 附带了一些 |
将会话属性另存为 JSON
默认情况下,Spring Session JDBC 将会话属性值保存为字节数组,这样的数组是属性值的 JDK 序列化的结果。
有时,以不同的格式保存会话属性很有用,例如 JSON,这可能在 RDBMS 中具有本机支持,从而在 SQL 查询中实现更好的函数和运算符兼容性。
在此示例中,我们将使用 PostgreSQL 作为 RDBMS,并使用 JSON 而不是 JDK 序列化来序列化会话属性值。
我们首先创建SPRING_SESSION_ATTRIBUTES
table 中带有jsonb
type 的attribute_values
列。
-
SQL
CREATE TABLE SPRING_SESSION
(
-- ...
);
-- indexes...
CREATE TABLE SPRING_SESSION_ATTRIBUTES
(
-- ...
ATTRIBUTE_BYTES JSONB NOT NULL,
-- ...
);
要自定义属性值的序列化方式,首先我们需要向 Spring Session JDBC 提供一个习惯ConversionService
负责从Object
自byte[]
反之亦然。
为此,我们可以创建一个ConversionService
叫springSessionConversionService
.
-
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
Inject the ObjectMapper
that is used by default in the application.
You can create a new one if you prefer.
2
Create a copy of that ObjectMapper
so we only apply the changes to the copy.
3
Since we are using Spring Security, we must register its Jackson Modules that tells Jackson how to properly serialize/deserialize Spring Security’s objects.
You might need to do the same for other objects that are persisted in the session.
4
Add the JsonSerializer
/JsonDeserializer
that we created into the ConversionService
.
Now that we configured how Spring Session JDBC converts our attributes values into byte[]
, we must customize the query that inserts and updates the session attributes.
The customization is necessary because Spring Session JDBC sets content as bytes in the SQL statement, however, bytea
is not compatible with jsonb
, therefore we need to encode the bytea
value to text and then convert it to 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
Uses the PostgreSQL encode function to convert from bytea
to text
And that’s it, you should now be able to see the session attributes saved as JSON in the database.
There is a sample available where you can see the whole implementation and run the tests.
If your UserDetails
implementation extends Spring Security’s org.springframework.security.core.userdetails.User
class, it is important that you register a custom deserializer for it.
Otherwise, Jackson will use the existing org.springframework.security.jackson2.UserDeserializer
which won’t result in the expected UserDetails
implementation. See gh-3009 for more details.
Specifying an alternative DataSource
By default, Spring Session JDBC uses the primary DataSource
bean that is available in the application.
However, there are some scenarios where an application might have multiple DataSource
s beans, in such scenarios you can tell Spring Session JDBC which DataSource
to use by qualifying the bean with @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
We annotate the dataSourceTwo
bean with @SpringSessionDataSource
to tell Spring Session JDBC that it should use that bean as the DataSource
.
Customizing How Spring Session JDBC Uses Transactions
All JDBC operations are performed in a transactional manner.
Transactions are performed with propagation set to REQUIRES_NEW
in order to avoid unexpected behavior due to interference with existing transactions (for example, running a save operation in a thread that already participates in a read-only transaction).
To customize how Spring Session JDBC uses transactions, you can provide a TransactionOperations
bean named springSessionTransactionOperations
.
For example, if you want to disable transactions as a whole, you can do:
-
Java
import org.springframework.transaction.support.TransactionOperations;
@Configuration
@EnableJdbcHttpSession
public class SessionConfig {
@Bean("springSessionTransactionOperations")
public TransactionOperations springSessionTransactionOperations() {
return TransactionOperations.withoutTransaction();
}
}
If you want more control, you can also provide the TransactionManager
that is used by the configured TransactionTemplate
.
By default, Spring Session will try to resolve the primary TransactionManager
bean from the application context.
In some scenarios, for example when there are multiple DataSource
s, it is very likely that there will be multiple TransactionManager
s, you can tell which TransactionManager
bean that you want to use with Spring Session JDBC by qualifying it with @SpringSessionTransactionManager
:
-
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
In order to avoid overloading your database with expired sessions, Spring Session JDBC executes a clean-up job every minute that deletes the expired sessions (and its attributes).
There are several reasons that you might want to customize the clean-up job, let’s see the most common in the following sections.
However, the customizations on the default job are limited, and that is intentional, Spring Session is not meant to provide a robust batch processing since there are a lot of frameworks or libraries that do a better job at that.
Therefore, if you want more customization power, consider disabling the default job and providing your own.
A good alternative is to use Spring Batch which provides a robust solution for batch processing applications.
Customizing How Often Expired Sessions Are Cleaned Up
You can customize the cron expression that defines how often the clean-up job runs by using the cleanupCron
attribute in @EnableJdbcHttpSession
:
-
Java
@Configuration
@EnableJdbcHttpSession(cleanupCron = "0 0 * * * *") // top of every hour of every day
public class SessionConfig {
}
Or, if you are using Spring Boot, set the spring.session.jdbc.cleanup-cron
property:
-
application.properties
spring.session.jdbc.cleanup-cron="0 0 * * * *"
Disabling the Job
To disable the job you must pass Scheduled.CRON_DISABLED
to the cleanupCron
attribute in @EnableJdbcHttpSession
:
-
Java
@Configuration
@EnableJdbcHttpSession(cleanupCron = Scheduled.CRON_DISABLED)
public class SessionConfig {
}
Customizing the Delete By Expiry Time Query
You can customize the query that deletes expired sessions by using JdbcIndexedSessionRepository.setDeleteSessionsByExpiryTimeQuery
through a SessionRepositoryCustomizer<JdbcIndexedSessionRepository>
bean:
-
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'
""");
}
}