Skip to content

Commit fbc1b19

Browse files
christophstroblmp911de
authored andcommitted
Add declarative Redis listener sample.
1 parent e0bcd8c commit fbc1b19

10 files changed

Lines changed: 396 additions & 1 deletion

File tree

README.adoc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ Contains also examples running on Virtual Threads.
8888

8989
* `cluster` - Example for Redis Cluster support.
9090
* `example` - Example for basic Spring Data Redis setup.
91-
* `pubsub` - Example project to show Pub/Sub usage using Platform and Virtual Threads.
91+
* `listener` - Example to show declarative `@RedisListener` Pub/Sub.
92+
* `pubsub` - Example project to show programmatic Pub/Sub usage using Platform and Virtual Threads.
9293
* `reactive` - Example project to show reactive template support.
9394
* `repositories` - Example demonstrating Spring Data repository abstraction on top of Redis.
9495
* `sentinel` - Example for Redis Sentinel support.

redis/listener/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Spring Data Redis - Annotated Listener Example
2+
3+
This project demonstrates **declarative Redis Pub/Sub listeners**.
4+
5+
## Features
6+
7+
- `@RedisListener` on component methods to subscribe to Redis channels
8+
- `@EnableRedisListeners` to activate the listener infrastructure
9+
- Receiving **string** messages on a channel
10+
- Receiving **JSON** payloads deserialized to domain types (e.g. `Person`)
11+
12+
## Requirements
13+
14+
- **Docker** (for running integration tests with Testcontainers).
15+
16+
## Running the tests
17+
18+
Integration tests publish messages to Redis channels and assert that the annotated listeners receive and process them. They use Testcontainers to start a Redis container, so Docker must be running.
19+
20+
```bash
21+
cd redis/listener
22+
mvn test
23+
```

redis/listener/pom.xml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<project xmlns="http://maven.apache.org/POM/4.0.0"
2+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
<modelVersion>4.0.0</modelVersion>
5+
6+
<parent>
7+
<groupId>org.springframework.data.examples</groupId>
8+
<artifactId>spring-data-redis-examples</artifactId>
9+
<version>4.0.0-SNAPSHOT</version>
10+
<relativePath>../pom.xml</relativePath>
11+
</parent>
12+
13+
<artifactId>spring-data-redis-listener</artifactId>
14+
<name>Spring Data Redis - Annotated Listener</name>
15+
<description>Sample project for Spring Data Redis @RedisListener annotation-driven Pub/Sub</description>
16+
17+
<dependencies>
18+
19+
<dependency>
20+
<groupId>org.springframework</groupId>
21+
<artifactId>spring-messaging</artifactId>
22+
</dependency>
23+
24+
<dependency>
25+
<groupId>tools.jackson.core</groupId>
26+
<artifactId>jackson-databind</artifactId>
27+
</dependency>
28+
29+
</dependencies>
30+
31+
</project>
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright 2026-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package example.springdata.redis.listener;
17+
18+
import java.util.concurrent.BlockingQueue;
19+
import java.util.concurrent.LinkedBlockingQueue;
20+
21+
/**
22+
* Just a little helper to capture the already converted payload received from Redis Pub/Sub.
23+
*
24+
* @author Christoph Strobl
25+
*/
26+
public class Messages {
27+
28+
private final MessageCapturer<String> stringMessages = new MessageCapturer<>();
29+
private final MessageCapturer<Person> pojoMessages = new MessageCapturer<>();
30+
31+
public BlockingQueue<String> capturedStrings() {
32+
return stringMessages.captured();
33+
}
34+
35+
void capture(Person person) {
36+
pojoMessages.capture(person);
37+
}
38+
39+
void capture(String string) {
40+
stringMessages.capture(string);
41+
}
42+
43+
public BlockingQueue<Person> capturedPojos() {
44+
return pojoMessages.captured();
45+
}
46+
47+
/**
48+
* Holder for received messages.
49+
*/
50+
public static class MessageCapturer<T> {
51+
52+
private final BlockingQueue<T> received = new LinkedBlockingQueue<>();
53+
54+
void capture(T message) {
55+
received.add(message);
56+
}
57+
58+
public BlockingQueue<T> captured() {
59+
return received;
60+
}
61+
}
62+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2026-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package example.springdata.redis.listener;
17+
18+
import java.util.Objects;
19+
20+
import com.fasterxml.jackson.annotation.JsonCreator;
21+
import com.fasterxml.jackson.annotation.JsonProperty;
22+
23+
/**
24+
* Example domain type for JSON payloads in Redis Pub/Sub messages.
25+
*
26+
* @author Christoph Strobl
27+
*/
28+
public class Person {
29+
30+
private final String firstname;
31+
private final String lastname;
32+
33+
@JsonCreator
34+
public Person(@JsonProperty("firstname") String firstname, @JsonProperty("lastname") String lastname) {
35+
this.firstname = firstname;
36+
this.lastname = lastname;
37+
}
38+
39+
public String getFirstname() {
40+
return firstname;
41+
}
42+
43+
public String getLastname() {
44+
return lastname;
45+
}
46+
47+
48+
@Override
49+
public boolean equals(Object o) {
50+
if (o == this) {
51+
return true;
52+
}
53+
if (o == null || getClass() != o.getClass()) {
54+
return false;
55+
}
56+
Person person = (Person) o;
57+
return Objects.equals(firstname, person.firstname) && Objects.equals(lastname, person.lastname);
58+
}
59+
60+
@Override
61+
public int hashCode() {
62+
return Objects.hash(firstname, lastname);
63+
}
64+
65+
@Override
66+
public String toString() {
67+
return "Person{firstname='" + firstname + "', lastname='" + lastname + "'}";
68+
}
69+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Copyright 2026-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package example.springdata.redis.listener;
17+
18+
import org.springframework.beans.factory.annotation.Autowired;
19+
import org.springframework.boot.SpringApplication;
20+
import org.springframework.boot.autoconfigure.SpringBootApplication;
21+
import org.springframework.context.annotation.Bean;
22+
import org.springframework.data.redis.annotation.EnableRedisListeners;
23+
import org.springframework.data.redis.annotation.RedisListener;
24+
import org.springframework.data.redis.connection.RedisConnectionFactory;
25+
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
26+
import org.springframework.messaging.MessageHeaders;
27+
import org.springframework.messaging.handler.annotation.Payload;
28+
import org.springframework.stereotype.Component;
29+
30+
/**
31+
* Spring Boot application for the Redis annotated listener sample.
32+
*
33+
* @author Christoph Strobl
34+
*/
35+
@SpringBootApplication
36+
@EnableRedisListeners
37+
public class RedisListenerApplication {
38+
39+
public static void main(String[] args) {
40+
SpringApplication.run(RedisListenerApplication.class, args);
41+
}
42+
43+
/**
44+
* Compontent tying together declarative redis listeners and the capturing {@link Messages}
45+
* {@link RedisListenerApplication#messages() bean) that preserves received payload for verification in tests.
46+
*/
47+
@Component
48+
static class Listeners {
49+
50+
private final Messages messages;
51+
52+
Listeners(@Autowired Messages messages) {
53+
this.messages = messages;
54+
}
55+
56+
/**
57+
* Declarative channel listener capturing raw {@link String}s received from Redis via the {@literal string-channel}.
58+
*
59+
* @param data the payload received via the {@literal string-channel}.
60+
* @param headers {@link MessageHeaders} when receiving the message.
61+
*/
62+
@RedisListener(topic = "string-channel")
63+
public void processStringMessage(@Payload String data, MessageHeaders headers) {
64+
65+
System.out.printf("received message [%s] from [%s] at [%s].%n", data, headers.get("channel"), headers.getTimestamp());
66+
messages.capture(data);
67+
}
68+
69+
/**
70+
* Declarative channel listener capturing {@link Person} objects converted from JSON.
71+
*
72+
* @param person already converted {@link Person} object received from Redis via the {@literal person-channel}.
73+
*/
74+
@RedisListener(topic = "person-channel")
75+
public void processPersonMessage(Person person) {
76+
messages.capture(person);
77+
}
78+
}
79+
80+
81+
@Bean
82+
Messages messages() {
83+
return new Messages();
84+
}
85+
86+
/**
87+
* Provide required
88+
* {@link RedisMessageListenerContainer} for annotation-driven endpoints
89+
* {@link Listeners#processStringMessage(String, MessageHeaders)}} &
90+
* {@link Listeners#processPersonMessage(Person)}.
91+
*
92+
* @param connectionFactory must not be {@literal null}.
93+
* @return new instance of {@link RedisListenerApplication}.
94+
*/
95+
@Bean
96+
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) {
97+
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
98+
container.setConnectionFactory(connectionFactory);
99+
return container;
100+
}
101+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2026-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package example.springdata.redis.listener;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
import java.util.concurrent.TimeUnit;
21+
22+
import org.junit.jupiter.api.Test;
23+
import org.springframework.beans.factory.annotation.Autowired;
24+
import org.springframework.boot.test.context.SpringBootTest;
25+
import org.springframework.context.annotation.Import;
26+
import org.springframework.data.redis.core.StringRedisTemplate;
27+
import org.springframework.messaging.MessageHeaders;
28+
29+
/**
30+
* Integration tests for {@literal @RedisListener} annotation-driven endpoints.
31+
*
32+
* @author Christoph Strobl
33+
*/
34+
@SpringBootTest(classes = RedisListenerApplication.class)
35+
@Import(RedisTestConfiguration.class)
36+
class AnnotatedRedisListenerIntegrationTests {
37+
38+
@Autowired StringRedisTemplate redisTemplate;
39+
@Autowired Messages messages;
40+
41+
/**
42+
* Publish a raw String message to the {@literal string-channel} channel and verify that the message
43+
* is received by the {@link RedisListenerApplication.Listeners#processStringMessage(String, MessageHeaders) string listener}.
44+
*/
45+
@Test
46+
void shouldReceiveStringMessage() throws Exception {
47+
48+
redisTemplate.convertAndSend("string-channel", "Hello, world!");
49+
50+
String received = messages.capturedStrings().poll(5, TimeUnit.SECONDS);
51+
assertThat(received).isEqualTo("Hello, world!");
52+
}
53+
54+
/**
55+
* Publish a JSON String message to the {@literal person-channel} channel and verify that the payload is converted
56+
* and processed by the {@link RedisListenerApplication.Listeners#processPersonMessage(Person) person listener}.
57+
*/
58+
@Test
59+
void shouldReceiveJsonPersonMessage() throws Exception {
60+
61+
String json = "{\"firstname\":\"Homer\",\"lastname\":\"Simpson\"}";
62+
redisTemplate.convertAndSend("person-channel", json);
63+
64+
Person received = messages.capturedPojos().poll(5, TimeUnit.SECONDS);
65+
assertThat(received).isNotNull();
66+
assertThat(received.getFirstname()).isEqualTo("Homer");
67+
assertThat(received.getLastname()).isEqualTo("Simpson");
68+
}
69+
}

0 commit comments

Comments
 (0)