Skip to content

Commit

Permalink
GH-248 - Support to automatically externalize events.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
odrotbohm committed Sep 7, 2023
1 parent 971a143 commit 0906a2b
Show file tree
Hide file tree
Showing 58 changed files with 3,391 additions and 42 deletions.
15 changes: 15 additions & 0 deletions spring-modulith-bom/pom.xml
Expand Up @@ -39,6 +39,11 @@
<artifactId>spring-modulith-docs</artifactId>
<version>1.1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-events-amqp</artifactId>
<version>1.1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-events-core</artifactId>
Expand All @@ -54,11 +59,21 @@
<artifactId>spring-modulith-events-jdbc</artifactId>
<version>1.1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-events-jms</artifactId>
<version>1.1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-events-jpa</artifactId>
<version>1.1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-events-kafka</artifactId>
<version>1.1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-events-mongodb</artifactId>
Expand Down
7 changes: 5 additions & 2 deletions spring-modulith-events/pom.xml
Expand Up @@ -14,12 +14,15 @@
<name>Spring Modulith - Events</name>

<modules>
<module>spring-modulith-events-amqp</module>
<module>spring-modulith-events-api</module>
<module>spring-modulith-events-core</module>
<module>spring-modulith-events-jpa</module>
<module>spring-modulith-events-jackson</module>
<module>spring-modulith-events-jdbc</module>
<module>spring-modulith-events-jms</module>
<module>spring-modulith-events-jpa</module>
<module>spring-modulith-events-kafka</module>
<module>spring-modulith-events-mongodb</module>
<module>spring-modulith-events-jackson</module>
</modules>

<profiles>
Expand Down
92 changes: 92 additions & 0 deletions spring-modulith-events/spring-modulith-events-amqp/pom.xml
@@ -0,0 +1,92 @@
<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.modulith</groupId>
<artifactId>spring-modulith-events</artifactId>
<version>1.1.0-SNAPSHOT</version>
</parent>

<name>Spring Modulith - Events - AMQP support</name>
<artifactId>spring-modulith-events-amqp</artifactId>

<properties>
<module.name>org.springframework.modulith.events.amqp</module.name>
</properties>

<dependencies>

<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-api</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-events-core</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-amqp</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<optional>true</optional>
</dependency>

<!-- Test dependencies -->

<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-jdbc</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>rabbitmq</artifactId>
<scope>test</scope>
</dependency>

</dependencies>

</project>
@@ -0,0 +1,63 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.modulith.events.amqp;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.core.RabbitMessageOperations;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.modulith.events.EventExternalizationConfiguration;
import org.springframework.modulith.events.config.EventExternalizationAutoConfiguration;
import org.springframework.modulith.events.support.BrokerRouting;
import org.springframework.modulith.events.support.DelegatingEventExternalizer;

/**
* Auto-configuration to set up a {@link DelegatingEventExternalizer} to externalize events to RabbitMQ.
*
* @author Oliver Drotbohm
* @since 1.1
*/
@AutoConfiguration
@AutoConfigureAfter(EventExternalizationAutoConfiguration.class)
@ConditionalOnClass(RabbitTemplate.class)
class RabbitEventExternalizerConfiguration {

private static final Logger logger = LoggerFactory.getLogger(RabbitEventExternalizerConfiguration.class);

@Bean
DelegatingEventExternalizer rabbitEventExternalizer(EventExternalizationConfiguration configuration,
RabbitMessageOperations operations, BeanFactory factory) {

logger.debug("Registering domain event externalization to RabbitMQ…");

var context = new StandardEvaluationContext();
context.setBeanResolver(new BeanFactoryResolver(factory));

return new DelegatingEventExternalizer(configuration, (target, payload) -> {

var routing = BrokerRouting.of(target, context);

operations.convertAndSend(routing.getTarget(), routing.getKey(payload), payload);
});
}
}
@@ -0,0 +1,50 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.modulith.events.amqp;

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.amqp.RabbitTemplateCustomizer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;

import com.fasterxml.jackson.databind.ObjectMapper;

/**
* Auto-configuration to configure {@link RabbitTemplate} to use the Jackson {@link ObjectMapper} present in the
* application.
*
* @author Oliver Drotbohm
* @since 1.1
*/
@AutoConfiguration
@ConditionalOnClass({ RabbitTemplate.class, ObjectMapper.class })
@ConditionalOnProperty(name = "spring.modulith.events.rabbitmq.enable-json", havingValue = "true",
matchIfMissing = true)
class RabbitJacksonConfiguration {

@Bean
@ConditionalOnBean(ObjectMapper.class)
RabbitTemplateCustomizer rabbitTemplateCustomizer(ObjectMapper mapper) {

return template -> {
template.setMessageConverter(new Jackson2JsonMessageConverter(mapper));
};
}
}
@@ -0,0 +1,10 @@
{
"properties": [
{
"name": "spring.modulith.events.rabbitmq.json-enabled",
"type": "java.lang.boolean",
"description": "Whether to auto-configure RabbitTemplate to use JSON for message serialization.",
"defaultValue": "true"
}
]
}
@@ -0,0 +1,2 @@
org.springframework.modulith.events.amqp.RabbitEventExternalizerConfiguration
org.springframework.modulith.events.amqp.RabbitJacksonConfiguration
@@ -0,0 +1,111 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.modulith.events.amqp;

import static org.assertj.core.api.Assertions.*;

import lombok.RequiredArgsConstructor;

import org.junit.jupiter.api.Test;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.modulith.ApplicationModuleListener;
import org.springframework.modulith.events.Externalized;
import org.springframework.transaction.annotation.Transactional;
import org.testcontainers.containers.RabbitMQContainer;
import org.testcontainers.utility.DockerImageName;

/**
* Integration tests for RabbitMQ-based event publication.
*
* @author Oliver Drotbohm
*/
@SpringBootTest
class RabbitEventPublicationIntegrationTests {

@Autowired TestPublisher publisher;
@Autowired RabbitAdmin rabbit;

@SpringBootApplication
static class TestConfiguration {

@Bean
@ServiceConnection
RabbitMQContainer rabbitMqContainer() {
return new RabbitMQContainer(DockerImageName.parse("rabbitmq"));
}

@Bean
TestPublisher testPublisher(ApplicationEventPublisher publisher) {
return new TestPublisher(publisher);
}

@Bean
TestListener testListener() {
return new TestListener();
}
}

@Test
void publishesEventToRabbitMq() throws Exception {

var target = new FanoutExchange("target");
rabbit.declareExchange(target);

var queue = new Queue("queue");
rabbit.declareQueue(queue);

Binding binding = BindingBuilder.bind(queue).to(target);
rabbit.declareBinding(binding);

publisher.publishEvent();

Thread.sleep(200);

var info = rabbit.getQueueInfo("queue");

assertThat(info.getMessageCount()).isEqualTo(1);
}

@Externalized("target")
static class TestEvent {}

@RequiredArgsConstructor
static class TestPublisher {

private final ApplicationEventPublisher events;

@Transactional
void publishEvent() {
events.publishEvent(new TestEvent());
}
}

static class TestListener {

@ApplicationModuleListener
void on(TestEvent event) {}
}
}
@@ -0,0 +1,2 @@
spring.artemis.embedded.topics=target
spring.modulith.events.jdbc.schema-initialization.enabled=true

0 comments on commit 0906a2b

Please sign in to comment.