Skip to content

Commit 9b0062f

Browse files
committed
GH-248 - Support to automatically externalize events.
We now allow externalizing application events to a variety of message brokers through the addition of Spring Modulith modules for Kafka, AMQP and JMS to a user project's classpath. Which events shall be externalized and how they're supposed to be routed to the message broker can be configured through either annotations or via a configuration API declared as Spring bean. In case Jackson is on the classpath, we also add auto-configuration to use a Boot-configured ObjectMapper instance with the corresponding message broker client APIs to properly serialize and deserialize messages to JSON.
1 parent 971a143 commit 9b0062f

File tree

58 files changed

+3391
-42
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+3391
-42
lines changed

spring-modulith-bom/pom.xml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@
3939
<artifactId>spring-modulith-docs</artifactId>
4040
<version>1.1.0-SNAPSHOT</version>
4141
</dependency>
42+
<dependency>
43+
<groupId>org.springframework.modulith</groupId>
44+
<artifactId>spring-modulith-events-amqp</artifactId>
45+
<version>1.1.0-SNAPSHOT</version>
46+
</dependency>
4247
<dependency>
4348
<groupId>org.springframework.modulith</groupId>
4449
<artifactId>spring-modulith-events-core</artifactId>
@@ -54,11 +59,21 @@
5459
<artifactId>spring-modulith-events-jdbc</artifactId>
5560
<version>1.1.0-SNAPSHOT</version>
5661
</dependency>
62+
<dependency>
63+
<groupId>org.springframework.modulith</groupId>
64+
<artifactId>spring-modulith-events-jms</artifactId>
65+
<version>1.1.0-SNAPSHOT</version>
66+
</dependency>
5767
<dependency>
5868
<groupId>org.springframework.modulith</groupId>
5969
<artifactId>spring-modulith-events-jpa</artifactId>
6070
<version>1.1.0-SNAPSHOT</version>
6171
</dependency>
72+
<dependency>
73+
<groupId>org.springframework.modulith</groupId>
74+
<artifactId>spring-modulith-events-kafka</artifactId>
75+
<version>1.1.0-SNAPSHOT</version>
76+
</dependency>
6277
<dependency>
6378
<groupId>org.springframework.modulith</groupId>
6479
<artifactId>spring-modulith-events-mongodb</artifactId>

spring-modulith-events/pom.xml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@
1414
<name>Spring Modulith - Events</name>
1515

1616
<modules>
17+
<module>spring-modulith-events-amqp</module>
1718
<module>spring-modulith-events-api</module>
1819
<module>spring-modulith-events-core</module>
19-
<module>spring-modulith-events-jpa</module>
20+
<module>spring-modulith-events-jackson</module>
2021
<module>spring-modulith-events-jdbc</module>
22+
<module>spring-modulith-events-jms</module>
23+
<module>spring-modulith-events-jpa</module>
24+
<module>spring-modulith-events-kafka</module>
2125
<module>spring-modulith-events-mongodb</module>
22-
<module>spring-modulith-events-jackson</module>
2326
</modules>
2427

2528
<profiles>
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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+
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<groupId>org.springframework.modulith</groupId>
9+
<artifactId>spring-modulith-events</artifactId>
10+
<version>1.1.0-SNAPSHOT</version>
11+
</parent>
12+
13+
<name>Spring Modulith - Events - AMQP support</name>
14+
<artifactId>spring-modulith-events-amqp</artifactId>
15+
16+
<properties>
17+
<module.name>org.springframework.modulith.events.amqp</module.name>
18+
</properties>
19+
20+
<dependencies>
21+
22+
<dependency>
23+
<groupId>org.springframework.modulith</groupId>
24+
<artifactId>spring-modulith-api</artifactId>
25+
<version>${project.version}</version>
26+
</dependency>
27+
28+
<dependency>
29+
<groupId>org.springframework.modulith</groupId>
30+
<artifactId>spring-modulith-events-core</artifactId>
31+
<version>${project.version}</version>
32+
</dependency>
33+
34+
<dependency>
35+
<groupId>org.springframework.amqp</groupId>
36+
<artifactId>spring-amqp</artifactId>
37+
</dependency>
38+
39+
<dependency>
40+
<groupId>org.springframework.amqp</groupId>
41+
<artifactId>spring-rabbit</artifactId>
42+
<optional>true</optional>
43+
</dependency>
44+
45+
<dependency>
46+
<groupId>com.fasterxml.jackson.core</groupId>
47+
<artifactId>jackson-databind</artifactId>
48+
<optional>true</optional>
49+
</dependency>
50+
51+
<!-- Test dependencies -->
52+
53+
<dependency>
54+
<groupId>org.springframework.modulith</groupId>
55+
<artifactId>spring-modulith-starter-jdbc</artifactId>
56+
<version>${project.version}</version>
57+
<scope>test</scope>
58+
</dependency>
59+
60+
<dependency>
61+
<groupId>com.h2database</groupId>
62+
<artifactId>h2</artifactId>
63+
<scope>test</scope>
64+
</dependency>
65+
66+
<dependency>
67+
<groupId>org.springframework.boot</groupId>
68+
<artifactId>spring-boot-starter-json</artifactId>
69+
<scope>test</scope>
70+
</dependency>
71+
72+
<dependency>
73+
<groupId>org.springframework.boot</groupId>
74+
<artifactId>spring-boot-starter-test</artifactId>
75+
<scope>test</scope>
76+
</dependency>
77+
78+
<dependency>
79+
<groupId>org.springframework.boot</groupId>
80+
<artifactId>spring-boot-testcontainers</artifactId>
81+
<scope>test</scope>
82+
</dependency>
83+
84+
<dependency>
85+
<groupId>org.testcontainers</groupId>
86+
<artifactId>rabbitmq</artifactId>
87+
<scope>test</scope>
88+
</dependency>
89+
90+
</dependencies>
91+
92+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright 2023 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 org.springframework.modulith.events.amqp;
17+
18+
import org.slf4j.Logger;
19+
import org.slf4j.LoggerFactory;
20+
import org.springframework.amqp.rabbit.core.RabbitMessageOperations;
21+
import org.springframework.amqp.rabbit.core.RabbitTemplate;
22+
import org.springframework.beans.factory.BeanFactory;
23+
import org.springframework.boot.autoconfigure.AutoConfiguration;
24+
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
25+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
26+
import org.springframework.context.annotation.Bean;
27+
import org.springframework.context.expression.BeanFactoryResolver;
28+
import org.springframework.expression.spel.support.StandardEvaluationContext;
29+
import org.springframework.modulith.events.EventExternalizationConfiguration;
30+
import org.springframework.modulith.events.config.EventExternalizationAutoConfiguration;
31+
import org.springframework.modulith.events.support.BrokerRouting;
32+
import org.springframework.modulith.events.support.DelegatingEventExternalizer;
33+
34+
/**
35+
* Auto-configuration to set up a {@link DelegatingEventExternalizer} to externalize events to RabbitMQ.
36+
*
37+
* @author Oliver Drotbohm
38+
* @since 1.1
39+
*/
40+
@AutoConfiguration
41+
@AutoConfigureAfter(EventExternalizationAutoConfiguration.class)
42+
@ConditionalOnClass(RabbitTemplate.class)
43+
class RabbitEventExternalizerConfiguration {
44+
45+
private static final Logger logger = LoggerFactory.getLogger(RabbitEventExternalizerConfiguration.class);
46+
47+
@Bean
48+
DelegatingEventExternalizer rabbitEventExternalizer(EventExternalizationConfiguration configuration,
49+
RabbitMessageOperations operations, BeanFactory factory) {
50+
51+
logger.debug("Registering domain event externalization to RabbitMQ…");
52+
53+
var context = new StandardEvaluationContext();
54+
context.setBeanResolver(new BeanFactoryResolver(factory));
55+
56+
return new DelegatingEventExternalizer(configuration, (target, payload) -> {
57+
58+
var routing = BrokerRouting.of(target, context);
59+
60+
operations.convertAndSend(routing.getTarget(), routing.getKey(payload), payload);
61+
});
62+
}
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2023 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 org.springframework.modulith.events.amqp;
17+
18+
import org.springframework.amqp.rabbit.core.RabbitTemplate;
19+
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
20+
import org.springframework.boot.autoconfigure.AutoConfiguration;
21+
import org.springframework.boot.autoconfigure.amqp.RabbitTemplateCustomizer;
22+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
23+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
24+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
25+
import org.springframework.context.annotation.Bean;
26+
27+
import com.fasterxml.jackson.databind.ObjectMapper;
28+
29+
/**
30+
* Auto-configuration to configure {@link RabbitTemplate} to use the Jackson {@link ObjectMapper} present in the
31+
* application.
32+
*
33+
* @author Oliver Drotbohm
34+
* @since 1.1
35+
*/
36+
@AutoConfiguration
37+
@ConditionalOnClass({ RabbitTemplate.class, ObjectMapper.class })
38+
@ConditionalOnProperty(name = "spring.modulith.events.rabbitmq.enable-json", havingValue = "true",
39+
matchIfMissing = true)
40+
class RabbitJacksonConfiguration {
41+
42+
@Bean
43+
@ConditionalOnBean(ObjectMapper.class)
44+
RabbitTemplateCustomizer rabbitTemplateCustomizer(ObjectMapper mapper) {
45+
46+
return template -> {
47+
template.setMessageConverter(new Jackson2JsonMessageConverter(mapper));
48+
};
49+
}
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"properties": [
3+
{
4+
"name": "spring.modulith.events.rabbitmq.json-enabled",
5+
"type": "java.lang.boolean",
6+
"description": "Whether to auto-configure RabbitTemplate to use JSON for message serialization.",
7+
"defaultValue": "true"
8+
}
9+
]
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
org.springframework.modulith.events.amqp.RabbitEventExternalizerConfiguration
2+
org.springframework.modulith.events.amqp.RabbitJacksonConfiguration
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Copyright 2023 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 org.springframework.modulith.events.amqp;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
20+
import lombok.RequiredArgsConstructor;
21+
22+
import org.junit.jupiter.api.Test;
23+
import org.springframework.amqp.core.Binding;
24+
import org.springframework.amqp.core.BindingBuilder;
25+
import org.springframework.amqp.core.FanoutExchange;
26+
import org.springframework.amqp.core.Queue;
27+
import org.springframework.amqp.rabbit.core.RabbitAdmin;
28+
import org.springframework.beans.factory.annotation.Autowired;
29+
import org.springframework.boot.autoconfigure.SpringBootApplication;
30+
import org.springframework.boot.test.context.SpringBootTest;
31+
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
32+
import org.springframework.context.ApplicationEventPublisher;
33+
import org.springframework.context.annotation.Bean;
34+
import org.springframework.modulith.ApplicationModuleListener;
35+
import org.springframework.modulith.events.Externalized;
36+
import org.springframework.transaction.annotation.Transactional;
37+
import org.testcontainers.containers.RabbitMQContainer;
38+
import org.testcontainers.utility.DockerImageName;
39+
40+
/**
41+
* Integration tests for RabbitMQ-based event publication.
42+
*
43+
* @author Oliver Drotbohm
44+
*/
45+
@SpringBootTest
46+
class RabbitEventPublicationIntegrationTests {
47+
48+
@Autowired TestPublisher publisher;
49+
@Autowired RabbitAdmin rabbit;
50+
51+
@SpringBootApplication
52+
static class TestConfiguration {
53+
54+
@Bean
55+
@ServiceConnection
56+
RabbitMQContainer rabbitMqContainer() {
57+
return new RabbitMQContainer(DockerImageName.parse("rabbitmq"));
58+
}
59+
60+
@Bean
61+
TestPublisher testPublisher(ApplicationEventPublisher publisher) {
62+
return new TestPublisher(publisher);
63+
}
64+
65+
@Bean
66+
TestListener testListener() {
67+
return new TestListener();
68+
}
69+
}
70+
71+
@Test
72+
void publishesEventToRabbitMq() throws Exception {
73+
74+
var target = new FanoutExchange("target");
75+
rabbit.declareExchange(target);
76+
77+
var queue = new Queue("queue");
78+
rabbit.declareQueue(queue);
79+
80+
Binding binding = BindingBuilder.bind(queue).to(target);
81+
rabbit.declareBinding(binding);
82+
83+
publisher.publishEvent();
84+
85+
Thread.sleep(200);
86+
87+
var info = rabbit.getQueueInfo("queue");
88+
89+
assertThat(info.getMessageCount()).isEqualTo(1);
90+
}
91+
92+
@Externalized("target")
93+
static class TestEvent {}
94+
95+
@RequiredArgsConstructor
96+
static class TestPublisher {
97+
98+
private final ApplicationEventPublisher events;
99+
100+
@Transactional
101+
void publishEvent() {
102+
events.publishEvent(new TestEvent());
103+
}
104+
}
105+
106+
static class TestListener {
107+
108+
@ApplicationModuleListener
109+
void on(TestEvent event) {}
110+
}
111+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
spring.artemis.embedded.topics=target
2+
spring.modulith.events.jdbc.schema-initialization.enabled=true

0 commit comments

Comments
 (0)