* first commit for Spring Data Redis TTL

* BAEL-4779: Removed incorrect module

* removed mvn wrapper

* Reduced wait time to check expiration

* J17 -> J11

* spring boot redis added to JDK 9 and above profile

* pulled latest from master

* restored from master

* resolved from master

* Update pom.xml

* Update pom.xml

* using Collections api available in J15

* updated Integration test class name

---------

Co-authored-by: s9m33r <no-reply>
Co-authored-by: Loredana Crusoveanu <lore.crusoveanu@gmail.com>
This commit is contained in:
Sameer 2023-03-15 05:14:31 +05:30 committed by GitHub
parent f93f1d2c1e
commit 350a3c3575
16 changed files with 501 additions and 3 deletions

View File

@ -999,7 +999,7 @@
<module>spring-5-webflux-2</module>
<module>spring-activiti</module>
<module>spring-batch-2</module>
<module>spring-boot-modules/spring-caching-2</module>
<module>spring-boot-modules/spring-caching-2</module>
<module>spring-core-2</module>
<module>spring-core-3</module>
<module>spring-core-5</module>
@ -1022,6 +1022,7 @@
<module>webrtc</module>
<module>persistence-modules/java-mongodb</module>
<module>messaging-modules/spring-apache-camel</module>
<module>spring-boot-modules/spring-boot-redis</module>
</modules>
<properties>
@ -1258,7 +1259,7 @@
<module>spring-5-webflux-2</module>
<module>spring-activiti</module>
<module>spring-batch-2</module>
<module>spring-boot-modules/spring-caching-2</module>
<module>spring-boot-modules/spring-caching-2</module>
<module>spring-core-2</module>
<module>spring-core-3</module>
<module>spring-core-5</module>
@ -1281,6 +1282,7 @@
<module>persistence-modules/java-mongodb</module>
<module>libraries-2</module>
<module>messaging-modules/spring-apache-camel</module>
<module>spring-boot-modules/spring-boot-redis</module>
</modules>
<properties>

View File

@ -106,4 +106,4 @@
</dependencies>
</dependencyManagement>
</project>
</project>

View File

@ -0,0 +1,35 @@
HELP.md
target/
.mvn
mvnw
mvnw.cmd
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.baelding</groupId>
<artifactId>spring-boot-redis</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-redis</name>
<description>Demo project for Spring Boot with Spring Data Redis</description>
<properties>
<java.version>15</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>it.ozimov</groupId>
<artifactId>embedded-redis</artifactId>
<version>0.7.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
<version>6.0.3</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,13 @@
package com.baelding.springbootredis;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringBootRedisApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootRedisApplication.class, args);
}
}

View File

@ -0,0 +1,58 @@
package com.baelding.springbootredis.config;
import com.baelding.springbootredis.model.Session;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisKeyExpiredEvent;
import org.springframework.data.redis.core.RedisKeyValueAdapter;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.convert.KeyspaceConfiguration;
import org.springframework.data.redis.core.convert.MappingConfiguration;
import org.springframework.data.redis.core.index.IndexConfiguration;
import org.springframework.data.redis.core.mapping.RedisMappingContext;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.stereotype.Component;
import java.util.Collections;
@Configuration
@EnableRedisRepositories(enableKeyspaceEvents = RedisKeyValueAdapter.EnableKeyspaceEvents.ON_STARTUP)
@Slf4j
public class RedisConfiguration {
@Bean
public RedisTemplate<String, Session> getRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Session> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
@Bean
public RedisMappingContext keyValueMappingContext() {
return new RedisMappingContext(new MappingConfiguration(new IndexConfiguration(), new MyKeyspaceConfiguration()));
}
public static class MyKeyspaceConfiguration extends KeyspaceConfiguration {
@Override
protected Iterable<KeyspaceSettings> initialConfiguration() {
KeyspaceSettings keyspaceSettings = new KeyspaceSettings(Session.class, "session");
keyspaceSettings.setTimeToLive(60L);
return Collections.singleton(keyspaceSettings);
}
}
@Component
public static class SessionExpiredEventListener {
@EventListener
public void handleRedisKeyExpiredEvent(RedisKeyExpiredEvent<Session> event) {
Session expiredSession = (Session) event.getValue();
assert expiredSession != null;
log.info("Session with key={} has expired", expiredSession.getId());
}
}
}

View File

@ -0,0 +1,49 @@
package com.baelding.springbootredis.controller;
import com.baelding.springbootredis.dto.SessionCreateRequest;
import com.baelding.springbootredis.model.Session;
import com.baelding.springbootredis.service.cache.session.SessionCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/v1/sessions")
public class SessionController {
private static final String LOCATION_HEADER_VALUE_FORMAT = "/v1/sessions/%s";
@Autowired
private SessionCache sessionCache;
@GetMapping
public List<Session> getAllSessions() {
return sessionCache.getAllSessions();
}
@GetMapping("/{id}")
public Session getSession(@PathVariable("id") String id) {
return sessionCache.getSession(id);
}
@PostMapping
public ResponseEntity<Session> createASession(@RequestBody SessionCreateRequest sessionCreateRequest) {
Session createdSession = sessionCache.createASession(Session.builder().expirationInSeconds(sessionCreateRequest.getExpirationInSeconds()).build());
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
headers.set(HttpHeaders.LOCATION, String.format(LOCATION_HEADER_VALUE_FORMAT, createdSession.getId()));
return new ResponseEntity<>(createdSession, new HttpHeaders(headers), HttpStatus.CREATED);
}
}

View File

@ -0,0 +1,14 @@
package com.baelding.springbootredis.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SessionCreateRequest {
private Long expirationInSeconds;
}

View File

@ -0,0 +1,12 @@
package com.baelding.springbootredis.exception.session;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class SessionNotFoundException extends RuntimeException {
private static final String MESSAGE_FORMAT = "Session with id=%s doesn't exists!";
public SessionNotFoundException(String id) {
super(String.format(MESSAGE_FORMAT, id));
}
}

View File

@ -0,0 +1,19 @@
package com.baelding.springbootredis.model;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.TimeToLive;
@Data
@Builder
@EqualsAndHashCode
@RedisHash(timeToLive = 60L)
public class Session {
@Id
private String id;
@TimeToLive
private Long expirationInSeconds;
}

View File

@ -0,0 +1,7 @@
package com.baelding.springbootredis.repository;
import com.baelding.springbootredis.model.Session;
import org.springframework.data.repository.CrudRepository;
public interface SessionRepository extends CrudRepository<Session, String> {
}

View File

@ -0,0 +1,34 @@
package com.baelding.springbootredis.service.cache.session;
import com.baelding.springbootredis.exception.session.SessionNotFoundException;
import com.baelding.springbootredis.model.Session;
import com.baelding.springbootredis.repository.SessionRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Iterator;
import java.util.List;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Service
public class RedisSessionCache implements SessionCache {
@Autowired
private SessionRepository sessionRepository;
@Override
public Session createASession(Session session) {
return sessionRepository.save(session);
}
@Override
public Session getSession(String id) {
return sessionRepository.findById(id).orElseThrow(() -> new SessionNotFoundException(id));
}
@Override
public List<Session> getAllSessions() {
return Stream.iterate(sessionRepository.findAll().iterator(), Iterator::hasNext, UnaryOperator.identity()).map(Iterator::next).collect(Collectors.toList());
}
}

View File

@ -0,0 +1,12 @@
package com.baelding.springbootredis.service.cache.session;
import com.baelding.springbootredis.model.Session;
import java.util.List;
public interface SessionCache {
Session createASession(Session session);
Session getSession(String id);
List<Session> getAllSessions();
}

View File

@ -0,0 +1,146 @@
package com.baelding.springbootredis;
import com.baelding.springbootredis.config.RedisTestConfiguration;
import com.baelding.springbootredis.dto.SessionCreateRequest;
import com.baelding.springbootredis.model.Session;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpHeaders;
import org.springframework.test.web.reactive.server.WebTestClient;
import java.util.List;
import java.util.Random;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = RedisTestConfiguration.class)
class SpringBootRedisApplicationIntegrationTest {
private static final String V1_SESSIONS_ENDPOINT = "/v1/sessions";
private static final String V1_GET_SESSION_BY_ID_ENDPOINT_TEMPLATE = V1_SESSIONS_ENDPOINT + "/%s";
@Autowired
private WebTestClient webTestClient;
private static Random random;
@BeforeAll
public static void beforeAll() {
random = new Random();
}
@Test
@DisplayName("""
WHEN get session endpoint is called
THEN return a success response.
""")
void shouldBeAbleToCallGetSessionsEndpoint() {
webTestClient.get().uri(V1_SESSIONS_ENDPOINT).exchange().expectStatus().isOk();
}
@Test
@DisplayName("""
WHEN requested to create a session with certain expiration value
THEN successfully create a session with the desired expiration.
""")
void shouldBeAbleToCreateASessionWithCertainExpiration() {
Long expirationInSeconds = 10L;
SessionCreateRequest sessionCreateRequest = SessionCreateRequest.builder().expirationInSeconds(expirationInSeconds).build();
Session session = webTestClient.post().uri(V1_SESSIONS_ENDPOINT).bodyValue(sessionCreateRequest).exchange().expectStatus().isCreated().expectHeader().exists(HttpHeaders.LOCATION).expectBody(Session.class).returnResult().getResponseBody();
Assertions.assertNotNull(session);
Assertions.assertEquals(expirationInSeconds, session.getExpirationInSeconds());
}
@Test
@DisplayName("""
GIVEN one or multiple session exists
WHEN get sessions endpoint is called
THEN return the all the existing session.
""")
void shouldBeAbleToGetTheExistingSessions() {
// Given
SessionCreateRequest session = SessionCreateRequest.builder().expirationInSeconds(300L).build();
int numberOfSessionsToCreate = random.nextInt(5);
List<Session> createdSessions = IntStream.range(0, numberOfSessionsToCreate)
.mapToObj(i -> webTestClient.post().uri(V1_SESSIONS_ENDPOINT).bodyValue(session).exchange().expectStatus().isCreated().expectBody(Session.class).returnResult().getResponseBody())
.collect(Collectors.toList());
// WHEN
webTestClient.get().uri(V1_SESSIONS_ENDPOINT).exchange().expectStatus()
// THEN
.isOk().expectBodyList(Session.class).value(sessions -> createdSessions.forEach(createdSession -> Assertions.assertTrue(sessions.contains(createdSession))));
}
@Test
@DisplayName("""
GIVEN a session is created with a given identifier
WHEN the session with the identifier is requested
THEN return the session
""")
void getSessionById() {
// GIVEN
SessionCreateRequest sessionCreateRequest = SessionCreateRequest.builder().expirationInSeconds(10L).build();
Session createdSession = webTestClient.post().uri(V1_SESSIONS_ENDPOINT).bodyValue(sessionCreateRequest).exchange().expectStatus().isCreated().expectBody(Session.class).returnResult().getResponseBody();
Assertions.assertNotNull(createdSession);
// WHEN
webTestClient.get().uri(String.format(V1_GET_SESSION_BY_ID_ENDPOINT_TEMPLATE, createdSession.getId())).exchange()
// THEN
.expectStatus().isOk().expectBody(Session.class).isEqualTo(createdSession);
}
@Test
@DisplayName("""
GIVEN a session identifier which doesn't exists
WHEN the session with the identifier is requested
THEN return 404 not found
""")
void getSessionByIdNotFound() {
// GIVEN
String sessionId = UUID.randomUUID().toString();
// WHEN
webTestClient.get().uri(String.format(V1_GET_SESSION_BY_ID_ENDPOINT_TEMPLATE, sessionId)).exchange()
// THEN
.expectStatus().isNotFound();
}
@Test
@DisplayName("""
GIVEN a session is created with a stipulated expiration
WHEN the session is requested before the expiration period
THEN return the session
WHEN the same session is requested after the expiration period
THEN return 404 not found
""")
void sessionExpiration() throws InterruptedException {
long expirationInSeconds = 3L;
// GIVEN
SessionCreateRequest sessionCreateRequest = SessionCreateRequest.builder().expirationInSeconds(expirationInSeconds).build();
Session createdSession = webTestClient.post().uri(V1_SESSIONS_ENDPOINT).bodyValue(sessionCreateRequest).exchange().expectStatus().isCreated().expectBody(Session.class).returnResult().getResponseBody();
Assertions.assertNotNull(createdSession);
// WHEN
webTestClient.get().uri(String.format(V1_GET_SESSION_BY_ID_ENDPOINT_TEMPLATE, createdSession.getId())).exchange()
// THEN
.expectStatus().isOk().expectBody(Session.class).isEqualTo(createdSession);
// WHEN
Thread.sleep(expirationInSeconds * 1000);
webTestClient.get().uri(String.format(V1_GET_SESSION_BY_ID_ENDPOINT_TEMPLATE, createdSession.getId())).exchange()
// THEN
.expectStatus().isNotFound();
}
}

View File

@ -0,0 +1,27 @@
package com.baelding.springbootredis.config;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.test.context.TestConfiguration;
import redis.embedded.RedisServer;
@TestConfiguration
public class RedisTestConfiguration {
private final RedisServer redisServer;
public RedisTestConfiguration(RedisProperties redisProperties) {
this.redisServer = new RedisServer(redisProperties.getPort());
}
@PostConstruct
public void postConstruct() {
redisServer.start();
}
@PreDestroy
public void preDestroy() {
redisServer.stop();
}
}