Redis Serialization Configuration and Session Event Handling in Spring Session
Spring Session offers extensive customization options for Redis-backed HTTP sessions. This article explores key configuration aspects including serialization strategies, namespace management, repository selection, and event handling mechanisms.
JSON Serialization Configuration
By default, Spring Session employs Java's native object serialization to persist session attribute values. While functional, this approach introduces several complications in distributed environments.
The Serializable Contract
Java serialization requires objects to implement the Serializable interface, converting them into binary byte arrays. Consider this example entity that will participate in session storage:
package com.example.demo.model.entity;
import java.io.Serializable;
public class UserProfile implements Serializable {
private static final long serialVersionUID = -8837526910472198746L;
private Integer userId;
private String displayName;
private Integer accessLevel;
public UserProfile() {}
public UserProfile(Integer userId, String displayName, Integer accessLevel) {
this.userId = userId;
this.displayName = displayName;
this.accessLevel = accessLevel;
}
// Getters and setters omitted for brevity
@Override
public String toString() {
return "UserProfile{userId=" + userId + ", displayName='" + displayName + "'}";
}
}
When a Spring Boot controller returns an object with @ResponseBody, the framework automatically leverages Jackson for JSON serialization. However, Java serialization operates differently—it produces binary output that can only be read back by compatible class definitions.
Limitations of Java Serialization
The default serialization mechanism becomes problematic in these scenarios:
- Version Incompatibility: When class definitions change between application deployments, deserialization fails with
InvalidClassException - Cross-Application Conflicts: Multiple applications sharing the same Redis instance may define classes with identical names but different structures
- Debugging Difficulty: Binary serialized data cannot be inspected directly in Redis CLI tools
Customizing JSON Serialization
Spring Data Redis provides GenericJackson2JsonRedisSerializer, which uses Jackson's ObjectMapper for transparent JSON conversion. This approach aligns with Spring Boot's default JSON handling and produces human-readable Redis values.
package com.example.demo.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
@Configuration
public class RedisSessionConfiguration {
/**
* Replaces default Java serialization with JSON-based serialization
* for all session attributes stored in Redis.
*/
@Bean
public RedisSerializer<Object> sessionAttributeSerializer(ObjectMapper mapper) {
return new GenericJackson2JsonRedisSerializer(mapper);
}
}
Namespace Configuration
When multiple applications share a Redis infrastructure, session data isolation becomes essential. Spring Session prefixes all session-related keys with a namespace string, preventing collisions between applications.
Default Namespace Behavior
The framework defaults to spring:session as the key prefix. All session data—including the session map, indexes, and expiration metadata—appears under this prefix in Redis.
Configuration Approaches
Application Properties Method:
spring.session.redis.namespace=production:user-service:sessions
Annotation-Based Method:
@SpringBootApplication
@EnableRedisHttpSession(redisNamespace = "production:user-service:sessions")
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
The @EnableRedisHttpSession annotation imports RedisHttpSessionConfiguration, which handles namespace application automatically.
SessionRepository Implementation Selection
The SessionRepository interface defines CRUD operations for session data. Spring Session provides multiple implementations with varying capabilities and performance characteristics.
RedisSessionRepository
This basic implementation stores session data as simple key-value pairs. Each session maps to a single Redis key containing all attributes as a serialized blob.
Characteristics:
- Minimal Redis overhead with simple key structure
- No secondary indexes for attribute-based queries
- Efficient for straightforward session storage needs
- Session retrieval requires the exact session identifier
RedisIndexedSessionRepository
This enhanced implementation maintains additional data structures enabling fast attribute-based lookups.
Additional Capabilities:
- Maintains indexes for principal names, custom attributes, and timestamps
- Supports querying sessions by user identity or other indexed properties
- Enables session expiration scheduling and cleanup
- Facilitates user session enumeration across devices
Query Methods:
@Autowired
private FindByIndexNameSessionRepository<? extends Session> sessionRepository;
public Collection<Session> findAllSessionsForUser(Principal user) {
return sessionRepository.findByPrincipalName(user.getName()).values();
}
public void terminateSpecificSession(Principal user, String sessionIdToRemove) {
Map<String, ? extends Session> userSessions =
sessionRepository.findByPrincipalName(user.getName());
if (userSessions.containsKey(sessionIdToRemove)) {
sessionRepository.deleteById(sessionIdToRemove);
}
}
Default Injection Behavior
Using @EnableRedisHttpSession automatically registers RedisIndexedSessionRepository as the session repository bean. This occurs through the imported RedisHttpSessionConfiguration class, which declares the indexed repository as the default bean.
Custom Repository Selection
To use the basic repository instead:
@Configuration
public class RedisSessionConfiguration {
@Bean
public SessionRepository sessionRepository(RedisTemplate<String, Object> template) {
return new RedisSessionRepository(template);
}
}
Session Event Handling
Many scenarios require responding to session lifecycle events—auditing user login activity, clearing cached resources when sessions terminate, or enforcing single-session-per-user policies. The RedisIndexedSessionRepository emits events throughout the session lifecycle.
Java Event Model Fundamentals
The event model involves three participants:
Event Source: The object triggering events, typically passed to event constructors
Event Object: Extends EventObject, carrying event-specific information
Event Listener: Extends EventListener, containing callback logic for event handling
class NotificationEvent extends EventObject {
private boolean processed;
public NotificationEvent(Object source) {
super(source);
}
public void setProcessed(boolean processed) {
this.processed = processed;
}
public boolean isProcessed() {
return processed;
}
}
@FunctionalInterface
interface NotificationListener extends EventListener {
void handleNotification(NotificationEvent event);
}
class NotificationDispatcher {
private static final List<NotificationListener> listeners = new ArrayList<>();
public static void registerListener(NotificationListener listener) {
listeners.add(listener);
}
public static void dispatch(NotificationEvent event) {
listeners.forEach(listener -> listener.handleNotification(event));
}
}
Spring Application Events
Spring Framework extends the event model with ApplicationEvent, which extends EventObject. The framework provides numerous event subtypes including context lifecycle events and custom application events.
ApplicationListener Interface:
@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
void onApplicationEvent(E event);
}
Implementation Example:
package com.example.demo.listener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import java.time.Instant;
@Slf4j
@Component
public class GlobalEventTracker implements ApplicationListener<ApplicationEvent> {
@Override
public void onApplicationEvent(ApplicationEvent event) {
log.info("Event occurred at {}: {}",
Instant.ofEpochMilli(event.getTimestamp()),
event.getClass().getSimpleName());
}
}
Spring Session Event Types
Spring Session defines a hierarchy of session-specific events under AbstractSessionEvent:
- SessionCreatedEvent: Triggered when a new session is established
- SessionDestroyedEvent: Base type for session termination; subtypes include:
- SessionExpiredEvent: Session reached its timeout threshold
- SessionDeletedEvent: Session explicitly invalidated or removed
Session Event Listener Implementation
Using @EventListener annotation for concise listener registration:
package com.example.demo.listener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
import org.springframework.session.events.*;
@Slf4j
@Configuration
public class SessionLifecycleHandler {
@EventListener(SessionCreatedEvent.class)
public void onSessionCreated(SessionCreatedEvent event) {
log.info("Session created: {}", event.getSessionId());
}
@EventListener(SessionDeletedEvent.class)
public void onSessionDeleted(SessionDeletedEvent event) {
log.info("Session deleted: {}", event.getSessionId());
}
@EventListener(SessionExpiredEvent.class)
public void onSessionExpired(SessionExpiredEvent event) {
log.info("Session expired: {}", event.getSessionId());
}
@EventListener(SessionDestroyedEvent.class)
public void onSessionDestroyed(SessionDestroyedEvent event) {
log.info("Session destroyed: {} (type: {})",
event.getSessionId(),
event.getClass().getSimpleName());
}
}
Practical Demonstration
The following controller creates and destroys sessions to demonstrate event emission:
package com.example.demo.controller;
import com.example.demo.model.entity.UserProfile;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpSession;
@Slf4j
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserSessionController {
private final RedisIndexedSessionRepository sessionRepository;
@PostMapping("/create")
public UserProfile createUserSession(HttpSession session,
@RequestParam("id") Integer userId) {
UserProfile profile = new UserProfile(userId, "JohnDoe", 2);
session.setAttribute("userProfile", profile);
log.info("Created user profile in session: {}", profile);
return profile;
}
@DeleteMapping("/session")
public void invalidateSession(HttpSession session) {
String sessionId = session.getId();
session.removeAttribute("userProfile");
log.info("Removed user profile from session");
sessionRepository.deleteById(sessionId);
}
}
Invoking these endpoints triggers corresponding session events processed by the registered listeners.