Skip to content

Restructure Redis HttpSession configuration support #2123

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
import org.springframework.session.data.SessionEventRegistry;
import org.springframework.session.data.redis.RedisIndexedSessionRepository.RedisSession;
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisOperations;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisIndexedHttpSession;
import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDestroyedEvent;
import org.springframework.test.context.ContextConfiguration;
Expand Down Expand Up @@ -691,7 +691,7 @@ private String getChangedSecurityName() {
}

@Configuration
@EnableRedisHttpSession(redisNamespace = "RedisIndexedSessionRepositoryITests", enableIndexingAndEvents = true)
@EnableRedisIndexedHttpSession(redisNamespace = "RedisIndexedSessionRepositoryITests")
static class Config extends BaseConfig {

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
class EnableRedisHttpSessionExpireSessionDestroyedTests<S extends Session> extends AbstractRedisITests {
class EnableRedisIndexedHttpSessionExpireSessionDestroyedTests<S extends Session> extends AbstractRedisITests {

@Autowired
private SessionRepository<S> repository;
Expand Down Expand Up @@ -113,7 +113,7 @@ void setLock(Object lock) {
}

@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1, enableIndexingAndEvents = true)
@EnableRedisIndexedHttpSession(maxInactiveIntervalInSeconds = 1)
static class Config extends BaseConfig {

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.session.data.redis.AbstractRedisITests;
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisOperations;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisIndexedHttpSession;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
Expand Down Expand Up @@ -101,7 +101,7 @@ boolean taskDispatched() throws InterruptedException {
}

@Configuration
@EnableRedisHttpSession(redisNamespace = "RedisListenerContainerTaskExecutorITests", enableIndexingAndEvents = true)
@EnableRedisIndexedHttpSession(redisNamespace = "RedisListenerContainerTaskExecutorITests")
static class Config extends BaseConfig {

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2014-2021 the original author or authors.
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -288,7 +288,7 @@ public class RedisIndexedSessionRepository

private byte[] expiredKeyPrefixBytes;

private final RedisOperations<Object, Object> sessionRedisOperations;
private final RedisOperations<String, Object> sessionRedisOperations;

private final RedisSessionExpirationPolicy expirationPolicy;

Expand All @@ -314,7 +314,7 @@ public class RedisIndexedSessionRepository
* @param sessionRedisOperations the {@link RedisOperations} to use for managing the
* sessions. Cannot be null.
*/
public RedisIndexedSessionRepository(RedisOperations<Object, Object> sessionRedisOperations) {
public RedisIndexedSessionRepository(RedisOperations<String, Object> sessionRedisOperations) {
Assert.notNull(sessionRedisOperations, "sessionRedisOperations cannot be null");
this.sessionRedisOperations = sessionRedisOperations;
this.expirationPolicy = new RedisSessionExpirationPolicy(sessionRedisOperations, this::getExpirationsKey,
Expand Down Expand Up @@ -406,7 +406,7 @@ private void configureSessionChannels() {
* Returns the {@link RedisOperations} used for sessions.
* @return the {@link RedisOperations} used for sessions
*/
public RedisOperations<Object, Object> getSessionRedisOperations() {
public RedisOperations<String, Object> getSessionRedisOperations() {
return this.sessionRedisOperations;
}

Expand Down Expand Up @@ -454,7 +454,7 @@ public Map<String, RedisSession> findByIndexNameAndIndexValue(String indexName,
* @return the Redis session
*/
private RedisSession getSession(String id, boolean allowExpired) {
Map<Object, Object> entries = getSessionBoundHashOperations(id).entries();
Map<String, Object> entries = getSessionBoundHashOperations(id).entries();
if (entries.isEmpty()) {
return null;
}
Expand All @@ -467,10 +467,10 @@ private RedisSession getSession(String id, boolean allowExpired) {
return result;
}

private MapSession loadSession(String id, Map<Object, Object> entries) {
private MapSession loadSession(String id, Map<String, Object> entries) {
MapSession loaded = new MapSession(id);
for (Map.Entry<Object, Object> entry : entries.entrySet()) {
String key = (String) entry.getKey();
for (Map.Entry<String, Object> entry : entries.entrySet()) {
String key = entry.getKey();
if (RedisSessionMapper.CREATION_TIME_KEY.equals(key)) {
loaded.setCreationTime(Instant.ofEpochMilli((long) entry.getValue()));
}
Expand Down Expand Up @@ -522,7 +522,7 @@ public void onMessage(Message message, byte[] pattern) {
if (ByteUtils.startsWith(messageChannel, this.sessionCreatedChannelPrefixBytes)) {
// TODO: is this thread safe?
@SuppressWarnings("unchecked")
Map<Object, Object> loaded = (Map<Object, Object>) this.defaultSerializer.deserialize(message.getBody());
Map<String, Object> loaded = (Map<String, Object>) this.defaultSerializer.deserialize(message.getBody());
handleCreated(loaded, new String(messageChannel));
return;
}
Expand Down Expand Up @@ -571,7 +571,7 @@ private void cleanupPrincipalIndex(RedisSession session) {
}
}

private void handleCreated(Map<Object, Object> loaded, String channel) {
private void handleCreated(Map<String, Object> loaded, String channel) {
String id = channel.substring(channel.lastIndexOf(":") + 1);
Session session = loadSession(id, loaded);
publishEvent(new SessionCreatedEvent(this, session));
Expand Down Expand Up @@ -661,7 +661,7 @@ public String getSessionExpiredChannel() {
* @param sessionId the id of the {@link Session} to work with
* @return the {@link BoundHashOperations} to operate on a {@link Session}
*/
private BoundHashOperations<Object, Object, Object> getSessionBoundHashOperations(String sessionId) {
private BoundHashOperations<String, String, Object> getSessionBoundHashOperations(String sessionId) {
String key = getSessionKey(sessionId);
return this.sessionRedisOperations.boundHashOps(key);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2014-2021 the original author or authors.
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -52,13 +52,13 @@ final class RedisSessionExpirationPolicy {

private static final String SESSION_EXPIRES_PREFIX = "expires:";

private final RedisOperations<Object, Object> redis;
private final RedisOperations<String, Object> redis;

private final Function<Long, String> lookupExpirationKey;

private final Function<String, String> lookupSessionKey;

RedisSessionExpirationPolicy(RedisOperations<Object, Object> sessionRedisOperations,
RedisSessionExpirationPolicy(RedisOperations<String, Object> sessionRedisOperations,
Function<Long, String> lookupExpirationKey, Function<String, String> lookupSessionKey) {
super();
this.redis = sessionRedisOperations;
Expand Down Expand Up @@ -96,7 +96,7 @@ void onExpirationUpdated(Long originalExpirationTimeInMilli, Session session) {
}

String expireKey = getExpirationKey(toExpire);
BoundSetOperations<Object, Object> expireOperations = this.redis.boundSetOps(expireKey);
BoundSetOperations<String, Object> expireOperations = this.redis.boundSetOps(expireKey);
expireOperations.add(keyToExpire);

long fiveMinutesAfterExpires = sessionExpireInSeconds + TimeUnit.MINUTES.toSeconds(5);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.session.data.redis.config.annotation.web.http;

import java.util.List;
import java.util.stream.Collectors;

import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.session.FlushMode;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.config.SessionRepositoryCustomizer;
import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration;
import org.springframework.session.data.redis.RedisSessionRepository;
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory;
import org.springframework.util.Assert;

/**
* Base configuration class for Redis based {@link SessionRepository} implementations.
*
* @param <T> the {@link SessionRepository} type
* @author Vedran Pavic
* @since 3.0.0
* @see RedisHttpSessionConfiguration
* @see RedisIndexedHttpSessionConfiguration
* @see SpringSessionRedisConnectionFactory
*/
@Configuration(proxyBeanMethods = false)
public abstract class AbstractRedisHttpSessionConfiguration<T extends SessionRepository<? extends Session>>
extends SpringHttpSessionConfiguration implements BeanClassLoaderAware {

private Integer maxInactiveIntervalInSeconds = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;

private String redisNamespace = RedisSessionRepository.DEFAULT_KEY_NAMESPACE;

private FlushMode flushMode = FlushMode.ON_SAVE;

private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;

private RedisConnectionFactory redisConnectionFactory;

private RedisSerializer<Object> defaultRedisSerializer;

private List<SessionRepositoryCustomizer<T>> sessionRepositoryCustomizers;

private ClassLoader classLoader;

public abstract T sessionRepository();

public void setMaxInactiveIntervalInSeconds(int maxInactiveIntervalInSeconds) {
this.maxInactiveIntervalInSeconds = maxInactiveIntervalInSeconds;
}

protected Integer getMaxInactiveIntervalInSeconds() {
return this.maxInactiveIntervalInSeconds;
}

public void setRedisNamespace(String namespace) {
Assert.hasText(namespace, "namespace must not be empty");
this.redisNamespace = namespace;
}

protected String getRedisNamespace() {
return this.redisNamespace;
}

public void setFlushMode(FlushMode flushMode) {
Assert.notNull(flushMode, "flushMode must not be null");
this.flushMode = flushMode;
}

protected FlushMode getFlushMode() {
return this.flushMode;
}

public void setSaveMode(SaveMode saveMode) {
Assert.notNull(saveMode, "saveMode must not be null");
this.saveMode = saveMode;
}

protected SaveMode getSaveMode() {
return this.saveMode;
}

@Autowired
public void setRedisConnectionFactory(
@SpringSessionRedisConnectionFactory ObjectProvider<RedisConnectionFactory> springSessionRedisConnectionFactory,
ObjectProvider<RedisConnectionFactory> redisConnectionFactory) {
this.redisConnectionFactory = springSessionRedisConnectionFactory
.getIfAvailable(redisConnectionFactory::getObject);
}

protected RedisConnectionFactory getRedisConnectionFactory() {
return this.redisConnectionFactory;
}

@Autowired(required = false)
@Qualifier("springSessionDefaultRedisSerializer")
public void setDefaultRedisSerializer(RedisSerializer<Object> defaultRedisSerializer) {
this.defaultRedisSerializer = defaultRedisSerializer;
}

protected RedisSerializer<Object> getDefaultRedisSerializer() {
return this.defaultRedisSerializer;
}

@Autowired(required = false)
public void setSessionRepositoryCustomizer(
ObjectProvider<SessionRepositoryCustomizer<T>> sessionRepositoryCustomizers) {
this.sessionRepositoryCustomizers = sessionRepositoryCustomizers.orderedStream().collect(Collectors.toList());
}

protected List<SessionRepositoryCustomizer<T>> getSessionRepositoryCustomizers() {
return this.sessionRepositoryCustomizers;
}

@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
}

protected RedisTemplate<String, Object> createRedisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
if (getDefaultRedisSerializer() != null) {
redisTemplate.setDefaultSerializer(getDefaultRedisSerializer());
}
redisTemplate.setConnectionFactory(getRedisConnectionFactory());
redisTemplate.setBeanClassLoader(this.classLoader);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,14 @@
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.RedisSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;

/**
* Add this annotation to an {@code @Configuration} class to expose the
* {@link SessionRepositoryFilter} as a bean named {@code springSessionRepositoryFilter}
* and backed by Redis. In order to leverage the annotation, a single
* {@link RedisConnectionFactory} must be provided. For example:
* and backed by {@link RedisSessionRepository}. In order to leverage the annotation, a
* single {@link RedisConnectionFactory} must be provided. For example:
*
* <pre class="code">
* &#064;Configuration
Expand All @@ -54,8 +53,7 @@
* }
* </pre>
*
* More advanced configurations can extend {@link RedisHttpSessionConfiguration} or
* {@link RedisIndexedHttpSessionConfiguration} instead.
* More advanced configurations can extend {@link RedisHttpSessionConfiguration} instead.
*
* @author Rob Winch
* @author Vedran Pavic
Expand All @@ -65,7 +63,7 @@
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(RedisHttpSessionConfigurationSelector.class)
@Import(RedisHttpSessionConfiguration.class)
@Configuration(proxyBeanMethods = false)
public @interface EnableRedisHttpSession {

Expand All @@ -86,7 +84,7 @@
* the applications and they could function within the same Redis instance.
* @return the unique namespace for keys
*/
String redisNamespace() default RedisIndexedSessionRepository.DEFAULT_NAMESPACE;
String redisNamespace() default RedisSessionRepository.DEFAULT_KEY_NAMESPACE;

/**
* Flush mode for the Redis sessions. The default is {@code ON_SAVE} which only
Expand All @@ -108,13 +106,4 @@
*/
SaveMode saveMode() default SaveMode.ON_SET_ATTRIBUTE;

/**
* Indicate whether the {@link SessionRepository} should publish session events and
* support fetching sessions by index. If true, a
* {@link RedisIndexedSessionRepository} will be used in place of
* {@link RedisSessionRepository}. This will result in slower performance.
* @return true if indexing and events should be enabled, false otherwise
*/
boolean enableIndexingAndEvents() default false;

}
Loading