Skip to content

Commit f657c78

Browse files
committed
Add Spring Cloud Event Externalization support
1 parent 013bd8b commit f657c78

27 files changed

+3959
-0
lines changed

spring-modulith-events/pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
<module>spring-modulith-events-messaging</module>
2626
<module>spring-modulith-events-mongodb</module>
2727
<module>spring-modulith-events-neo4j</module>
28+
<module>spring-modulith-events-scs</module>
2829
</modules>
2930

3031
<profiles>
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# Spring-Modulith Events Externalizer for Spring Cloud Stream
2+
3+
[![Maven Central](https://img.shields.io/maven-central/v/io.zenwave360.sdk/spring-modulith-events-scs.svg?label=Maven%20Central&logo=apachemaven)](https://search.maven.org/artifact/io.zenwave360.sdk/spring-modulith-events-scs)
4+
[![build](https://github.com/ZenWave360/spring-modulith-events-spring-cloud-stream/workflows/Build/badge.svg)](https://github.com/ZenWave360/spring-modulith-events-spring-cloud-stream/actions/workflows/build.yml)
5+
[![coverage](https://raw.githubusercontent.com/ZenWave360/spring-modulith-events-spring-cloud-stream/badges/jacoco.svg)](https://github.com/ZenWave360/spring-modulith-events-spring-cloud-stream/actions/workflows/build.yml)
6+
[![branches coverage](https://raw.githubusercontent.com/ZenWave360/spring-modulith-events-spring-cloud-stream/badges/branches.svg)](https://github.com/ZenWave360/spring-modulith-events-spring-cloud-stream/actions/workflows/build.yml)
7+
[![GitHub](https://img.shields.io/github/license/ZenWave360/spring-modulith-events-spring-cloud-stream)](https://github.com/ZenWave360/spring-modulith-events-spring-cloud-stream/blob/main/LICENSE)
8+
9+
Spring-Modulith Events Externalizer that uses Spring Cloud Stream supporting both JSON and Avro serialization formats.
10+
11+
## Getting Started
12+
13+
### Dependency
14+
Add the following Maven dependency to your project:
15+
16+
```xml
17+
<dependency>
18+
<groupId>io.zenwave360.sdk</groupId>
19+
<artifactId>spring-modulith-events-scs</artifactId>
20+
<version>${spring-modulith-events-scs.version}</version>
21+
</dependency>
22+
```
23+
24+
### Configuration
25+
Use `@EnableSpringCloudStreamEventExternalization` annotation to enable Spring Cloud Stream event externalization in your Spring configuration:
26+
27+
```java
28+
@Configuration
29+
@EnableSpringCloudStreamEventExternalization
30+
public class SpringCloudStreamEventsConfig {
31+
// Additional configurations (if needed)
32+
}
33+
```
34+
35+
This configuration ensures that, in addition to events annotated with `@Externalized`, all events of type `org.springframework.messaging.Message` with a header named `SpringCloudStreamEventExternalizer.SPRING_CLOUD_STREAM_EVENT_HEADER` will be externalized and routed to their specified destination using the value of this header as the routing target.
36+
37+
---
38+
39+
## Event Serialization
40+
41+
Using the transactional event publication log requires serializing events to a format that can be stored in a database. Since the generic type of `Message<?>` payload is lost when using the default `JacksonEventSerializer`, this library adds an extra `_class` field to preserve payload type information, allowing for complete deserialization to its original type.
42+
43+
This library provides support for POJO (JSON) and Avro serialization formats for `Message<?>` payloads.
44+
45+
### Avro Serialization
46+
47+
Avro serialization needs `com.fasterxml.jackson.dataformat.avro.AvroMapper` class present in the classpath. In order to use Avro serialization, you need to add the following dependency to your project:
48+
49+
```xml
50+
<dependency>
51+
<groupId>com.fasterxml.jackson.dataformat</groupId>
52+
<artifactId>jackson-dataformat-avro</artifactId>
53+
</dependency>
54+
```
55+
56+
---
57+
58+
## Routing Events
59+
60+
### Programmatic Routing for `Message<?`> events
61+
62+
You can define routing targets programmatically using a Message header:
63+
64+
```java
65+
public class CustomerEventsProducer implements ICustomerEventsProducer {
66+
67+
private final ApplicationEventPublisher applicationEventPublisher;
68+
69+
public void onCustomerCreated(CustomerCreated event) {
70+
Message<CustomerCreated> message = MessageBuilder.withPayload(event)
71+
.setHeader(
72+
SpringCloudStreamEventExternalizer.SPRING_CLOUD_STREAM_SENDTO_DESTINATION_HEADER,
73+
"customer-created") // <- target binding name
74+
.build();
75+
applicationEventPublisher.publishEvent(message);
76+
}
77+
}
78+
```
79+
80+
### Annotation-Based Routing for POJO Events
81+
82+
Leverage the `@Externalized` annotation to define the target binding name and routing key:
83+
84+
```java
85+
@Externalized("customer-created::#{#this.getLastname()}")
86+
class CustomerCreated {
87+
88+
public String getLastname() {
89+
// Return the customer's last name
90+
}
91+
}
92+
```
93+
94+
### Configure Spring Cloud Stream destination
95+
96+
Configure Spring Cloud Stream destination for your bindings as usual in `application.yml`:
97+
98+
```yaml
99+
spring:
100+
cloud:
101+
stream:
102+
bindings:
103+
customer-created:
104+
destination: customer-created-topic
105+
```
106+
107+
### Routing Key
108+
109+
`SpringCloudStreamEventExternalizer` dynamically sets the appropriate Message header (e.g., `kafka_messageKey` or `rabbit_routingKey`) from your routing key based on the channel binder type, if the routing header is not already present.
110+
111+
- KafkaMessageChannelBinder: `kafka_messageKey`
112+
- RabbitMessageChannelBinder: `rabbit_routingKey`
113+
- KinesisMessageChannelBinder: `partitionKey`
114+
- PubSubMessageChannelBinder: `pubsub_orderingKey`
115+
- EventHubsMessageChannelBinder: `partitionKey`
116+
- SolaceMessageChannelBinder: `solace_messageKey`
117+
- PulsarMessageChannelBinder: `pulsar_key`
118+
119+
---
120+
121+
## Using Snapshot Versions
122+
In order to test snapshot versions of this library, add the following repository to your Maven configuration:
123+
124+
```xml
125+
<repository>
126+
<id>gh</id>
127+
<url>https://raw.githubusercontent.com/ZenWave360/maven-snapshots/refs/heads/main</url>
128+
<snapshots>
129+
<enabled>true</enabled>
130+
</snapshots>
131+
</repository>
132+
```
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
3+
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
<modelVersion>4.0.0</modelVersion>
5+
6+
<parent>
7+
<groupId>org.springframework.modulith</groupId>
8+
<artifactId>spring-modulith-events</artifactId>
9+
<version>1.4.0-SNAPSHOT</version>
10+
</parent>
11+
12+
<name>Spring Modulith - Events - Spring Cloud Stream support</name>
13+
<artifactId>spring-modulith-events-scs</artifactId>
14+
15+
<properties>
16+
<module.name>org.springframework.modulith.events.scs</module.name>
17+
18+
<!-- integration testing versions -->
19+
<spring-boot.version>3.4.0</spring-boot.version>
20+
<spring-cloud.version>2024.0.0</spring-cloud.version>
21+
<spring-cloud-stream-schema.version>2.2.1.RELEASE</spring-cloud-stream-schema.version>
22+
<avro.version>1.11.4</avro.version>
23+
24+
<maven-surefire-plugin.version>3.0.0-M5</maven-surefire-plugin.version>
25+
</properties>
26+
27+
<developers>
28+
<developer>
29+
<name>Ivan Garcia Sainz-Aja</name>
30+
<email>[email protected]</email>
31+
<organization>ZenWave360</organization>
32+
<organizationUrl>https://github.com/ZenWave360</organizationUrl>
33+
</developer>
34+
</developers>
35+
36+
<dependencyManagement>
37+
<dependencies>
38+
<dependency>
39+
<groupId>org.springframework.boot</groupId>
40+
<artifactId>spring-boot-dependencies</artifactId>
41+
<version>${spring-boot.version}</version>
42+
<type>pom</type>
43+
<scope>import</scope>
44+
</dependency>
45+
<dependency>
46+
<groupId>org.springframework.cloud</groupId>
47+
<artifactId>spring-cloud-dependencies</artifactId>
48+
<version>${spring-cloud.version}</version>
49+
<type>pom</type>
50+
<scope>import</scope>
51+
</dependency>
52+
</dependencies>
53+
</dependencyManagement>
54+
55+
<dependencies>
56+
<dependency>
57+
<groupId>org.springframework.modulith</groupId>
58+
<artifactId>spring-modulith-api</artifactId>
59+
<version>${project.parent.version}</version>
60+
</dependency>
61+
<dependency>
62+
<groupId>org.springframework.modulith</groupId>
63+
<artifactId>spring-modulith-events-core</artifactId>
64+
<version>${project.parent.version}</version>
65+
</dependency>
66+
<dependency>
67+
<groupId>org.springframework.cloud</groupId>
68+
<artifactId>spring-cloud-stream</artifactId>
69+
</dependency>
70+
71+
<!-- optional -->
72+
<dependency>
73+
<groupId>org.apache.avro</groupId>
74+
<artifactId>avro</artifactId>
75+
<version>${avro.version}</version>
76+
<optional>true</optional>
77+
</dependency>
78+
<dependency>
79+
<groupId>com.fasterxml.jackson.dataformat</groupId>
80+
<artifactId>jackson-dataformat-avro</artifactId>
81+
<optional>true</optional>
82+
</dependency>
83+
<dependency>
84+
<groupId>com.fasterxml.jackson.core</groupId>
85+
<artifactId>jackson-databind</artifactId>
86+
<optional>true</optional>
87+
</dependency>
88+
89+
<!-- Testing -->
90+
<dependency>
91+
<groupId>org.springframework.boot</groupId>
92+
<artifactId>spring-boot-starter-test</artifactId>
93+
<scope>test</scope>
94+
</dependency>
95+
<dependency>
96+
<groupId>org.springframework.boot</groupId>
97+
<artifactId>spring-boot-starter-jdbc</artifactId>
98+
<scope>test</scope>
99+
</dependency>
100+
<dependency>
101+
<groupId>com.h2database</groupId>
102+
<artifactId>h2</artifactId>
103+
<scope>test</scope>
104+
</dependency>
105+
106+
<dependency>
107+
<groupId>org.springframework.cloud</groupId>
108+
<artifactId>spring-cloud-starter-stream-kafka</artifactId>
109+
<scope>test</scope>
110+
</dependency>
111+
<dependency>
112+
<groupId>org.springframework.kafka</groupId>
113+
<artifactId>spring-kafka-test</artifactId>
114+
<scope>test</scope>
115+
</dependency>
116+
117+
<dependency>
118+
<groupId>org.springframework.modulith</groupId>
119+
<artifactId>spring-modulith-starter-jdbc</artifactId>
120+
<version>${parent.version}</version>
121+
<scope>test</scope>
122+
</dependency>
123+
</dependencies>
124+
125+
<build>
126+
<plugins>
127+
<plugin>
128+
<groupId>org.apache.maven.plugins</groupId>
129+
<artifactId>maven-surefire-plugin</artifactId>
130+
<version>${maven-surefire-plugin.version}</version>
131+
<configuration>
132+
<!-- Force alphabetical order to have a reproducible build -->
133+
<runOrder>alphabetical</runOrder>
134+
<excludes>
135+
<exclude>**/*IT*</exclude>
136+
<exclude>**/*IntTest*</exclude>
137+
</excludes>
138+
</configuration>
139+
</plugin>
140+
</plugins>
141+
</build>
142+
143+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package org.springframework.modulith.events.scs;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import com.fasterxml.jackson.databind.node.ObjectNode;
5+
import com.fasterxml.jackson.dataformat.avro.AvroMapper;
6+
import org.springframework.modulith.events.core.EventSerializer;
7+
8+
import java.util.Map;
9+
10+
public class AvroEventSerializer extends MessageEventSerializer implements EventSerializer {
11+
12+
private AvroMapper avroMapper;
13+
14+
public AvroEventSerializer(ObjectMapper jacksonMapper) {
15+
super(jacksonMapper);
16+
this.avroMapper = AvroMapper.builder().build();
17+
}
18+
19+
public AvroEventSerializer(AvroMapper avroMapper, ObjectMapper jacksonMapper) {
20+
super(jacksonMapper);
21+
this.avroMapper = avroMapper;
22+
}
23+
24+
protected Map<String, Object> serializeToMap(Object payload) {
25+
ObjectNode objectNode = avroMapper.valueToTree(payload);
26+
objectNode.remove("specificData"); // TODO: remove this recursively
27+
return avroMapper.convertValue(objectNode, Map.class);
28+
}
29+
30+
}

0 commit comments

Comments
 (0)