Adding producer and consumer app examples for Spring Boot integration (#1208)

* adding spring boot producer

Signed-off-by: salaboy <Salaboy@gmail.com>

* adding consumer app

Signed-off-by: salaboy <Salaboy@gmail.com>

* increasing wait for events to popup

Signed-off-by: salaboy <Salaboy@gmail.com>

* adding readme and examples

Signed-off-by: salaboy <Salaboy@gmail.com>

* aligning tests for examples

Signed-off-by: salaboy <Salaboy@gmail.com>

* increasing time out

Signed-off-by: salaboy <Salaboy@gmail.com>

* adding health check from the sidecar

Signed-off-by: salaboy <Salaboy@gmail.com>

* feat: Adding basic HTTPEndpoint configuration support in testcontainers module (#1210)

* feat: Adding basic HTTPEndpoint configuration support in testcontainers module

Signed-off-by: Laurent Broudoux <laurent.broudoux@gmail.com>

* feat: #1209 Adding test for HTTPEndpoint in testcontainers module

Signed-off-by: Laurent Broudoux <laurent.broudoux@gmail.com>

---------

Signed-off-by: Laurent Broudoux <laurent.broudoux@gmail.com>
Signed-off-by: salaboy <Salaboy@gmail.com>

* updating example

Signed-off-by: salaboy <Salaboy@gmail.com>

* fixing example

Signed-off-by: salaboy <Salaboy@gmail.com>

* Add app health check support to Dapr Testcontainer (#1213)

* Add app health check support to Dapr Testcontainer

Signed-off-by: Artur Ciocanu <ciocanu@adobe.com>

* Some minor cleanup

Signed-off-by: Artur Ciocanu <ciocanu@adobe.com>

* Move waiting to beforeEach, it looks more natural

Signed-off-by: Artur Ciocanu <ciocanu@adobe.com>

---------

Signed-off-by: Artur Ciocanu <ciocanu@adobe.com>
Co-authored-by: Artur Ciocanu <ciocanu@adobe.com>
Signed-off-by: salaboy <Salaboy@gmail.com>

* commenting reuse

Signed-off-by: salaboy <Salaboy@gmail.com>

* adding how to run on Kubernetes, unfortunately we need to create containers

Signed-off-by: salaboy <Salaboy@gmail.com>

* removing subscription and fixing scopes

Signed-off-by: salaboy <Salaboy@gmail.com>

* adding license headers and logger

Signed-off-by: salaboy <Salaboy@gmail.com>

* updating logs

Signed-off-by: salaboy <Salaboy@gmail.com>

* updating READMEs and update_sdk_version for new module

Signed-off-by: salaboy <Salaboy@gmail.com>

* removing old line

Signed-off-by: salaboy <Salaboy@gmail.com>

* removing sleeps, using Wait

Signed-off-by: salaboy <Salaboy@gmail.com>

* updating Kubernetes tutorial to use local registry with KIND, and provide steps to create containers with spring boot

Signed-off-by: salaboy <Salaboy@gmail.com>

* adding new lines and formatting

Signed-off-by: salaboy <Salaboy@gmail.com>

* updating reuse and removing comments

Signed-off-by: salaboy <Salaboy@gmail.com>

* fixing reuse

Signed-off-by: salaboy <Salaboy@gmail.com>

* removing line break

Signed-off-by: salaboy <Salaboy@gmail.com>

* fixing custom line breaks

Signed-off-by: salaboy <Salaboy@gmail.com>

* fixing xml indent to 2 spaces

Signed-off-by: salaboy <Salaboy@gmail.com>

* Update spring-boot-examples/README.md

Co-authored-by: Cassie Coyle <cassie.i.coyle@gmail.com>
Signed-off-by: salaboy <Salaboy@gmail.com>

* Update spring-boot-examples/README.md

Co-authored-by: Cassie Coyle <cassie.i.coyle@gmail.com>
Signed-off-by: salaboy <Salaboy@gmail.com>

* Update spring-boot-examples/README.md

Co-authored-by: Cassie Coyle <cassie.i.coyle@gmail.com>
Signed-off-by: salaboy <Salaboy@gmail.com>

* Update spring-boot-examples/README.md

Co-authored-by: Cassie Coyle <cassie.i.coyle@gmail.com>
Signed-off-by: salaboy <Salaboy@gmail.com>

* Update spring-boot-examples/README.md

Co-authored-by: Cassie Coyle <cassie.i.coyle@gmail.com>
Signed-off-by: salaboy <Salaboy@gmail.com>

* Update spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/ConsumerAppTests.java

Co-authored-by: Cassie Coyle <cassie.i.coyle@gmail.com>
Signed-off-by: salaboy <Salaboy@gmail.com>

* Update spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/TestSubscriberRestController.java

Co-authored-by: Cassie Coyle <cassie.i.coyle@gmail.com>
Signed-off-by: salaboy <Salaboy@gmail.com>

* adding license header to missing files

Signed-off-by: salaboy <Salaboy@gmail.com>

* adding automated testing for spring boot example

Signed-off-by: salaboy <Salaboy@gmail.com>

* adding sb examples to the validation pipeline

Signed-off-by: salaboy <Salaboy@gmail.com>

* updating timeouts

Signed-off-by: salaboy <Salaboy@gmail.com>

* updating return codes

Signed-off-by: salaboy <Salaboy@gmail.com>

---------

Signed-off-by: salaboy <Salaboy@gmail.com>
Signed-off-by: Laurent Broudoux <laurent.broudoux@gmail.com>
Signed-off-by: Artur Ciocanu <ciocanu@adobe.com>
Signed-off-by: Artur Souza <artursouza.ms@outlook.com>
Co-authored-by: Laurent Broudoux <laurent.broudoux@gmail.com>
Co-authored-by: artur-ciocanu <artur.ciocanu@gmail.com>
Co-authored-by: Artur Ciocanu <ciocanu@adobe.com>
Co-authored-by: Cassie Coyle <cassie.i.coyle@gmail.com>
Co-authored-by: Cassie Coyle <cassie@diagrid.io>
Co-authored-by: Artur Souza <artursouza.ms@outlook.com>
This commit is contained in:
salaboy 2025-03-01 04:33:40 +00:00 committed by GitHub
parent 59abd5dbcc
commit 5dbeafc24a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 2047 additions and 17 deletions

View File

@ -27,4 +27,7 @@ mvn versions:set -DnewVersion=$DAPR_JAVA_SDK_ALPHA_VERSION -f testcontainers-dap
# dapr-spring
mvn versions:set -DnewVersion=$DAPR_JAVA_SDK_ALPHA_VERSION -f dapr-spring/pom.xml
# spring-boot-examples
mvn versions:set -DnewVersion=$DAPR_JAVA_SDK_ALPHA_VERSION -f spring-boot-examples/pom.xml
git clean -f

View File

@ -164,3 +164,7 @@ jobs:
working-directory: ./examples
run: |
mm.py ./src/main/java/io/dapr/examples/pubsub/stream/README.md
- name: Validate Spring Boot examples
working-directory: ./spring-boot-examples
run: |
mm.py README.md

View File

@ -9,6 +9,7 @@ This is the Dapr SDK for Java, including the following features:
* Binding
* State Store
* Actors
* Workflows
## Getting Started
@ -112,6 +113,13 @@ Try the following examples to learn more about Dapr's Java SDK:
* [Exception handling](./examples/src/main/java/io/dapr/examples/exception)
* [Unit testing](./examples/src/main/java/io/dapr/examples/unittesting)
### Running Spring Boot examples
The Spring Boot integration for Dapr use [Testcontainers](https://testcontainers.com) to set up a local environment development flow that doesn't
require the use of the `dapr` CLI and it integrates with the Spring Boot programming model.
You can find a [step-by-step tutorial showing this integration here](./spring-boot-examples/README.md).
### API Documentation
Please, refer to our [Javadoc](https://dapr.github.io/java-sdk/) website.

View File

@ -337,6 +337,7 @@
<module>sdk-springboot</module>
<module>dapr-spring</module>
<module>examples</module>
<module>spring-boot-examples</module>
<!-- We are following test containers artifact convention on purpose, don't rename -->
<module>testcontainers-dapr</module>
</modules>

View File

@ -65,6 +65,7 @@ public class DaprContainerIT {
private static final DaprContainer DAPR_CONTAINER = new DaprContainer(IMAGE_TAG)
.withAppName("dapr-app")
.withAppPort(8081)
.withAppHealthCheckPath("/actuator/health")
.withAppChannelAddress("host.testcontainers.internal");
/**
@ -77,6 +78,9 @@ public class DaprContainerIT {
}
private void configStub() {
stubFor(any(urlMatching("/actuator/health"))
.willReturn(aResponse().withBody("[]").withStatus(200)));
stubFor(any(urlMatching("/dapr/subscribe"))
.willReturn(aResponse().withBody("[]").withStatus(200)));

View File

@ -0,0 +1,176 @@
# Dapr Spring Boot and Testcontainers integration Example
This example consists of two applications:
- Producer App:
- Publish messages using a Spring Messaging approach
- Store and retrieve information using Spring Data CrudRepository
- Implements a Workflow with Dapr Workflows
- Consumer App:
- Subscribe to messages
## Running these examples from source code
To run these examples you will need:
- Java SDK
- Maven
- Docker or a container runtime such as Podman
From the `spring-boot-examples/` directory you can start each service using the test configuration that uses
[Testcontainers](https://testcontainers.com) to boostrap [Dapr](https://dapr.io) by running the following command:
<!-- STEP
name: Run Demo Producer Service
match_order: none
output_match_mode: substring
expected_stdout_lines:
- 'Started ProducerApplication'
background: true
expected_return_code: 143
sleep: 30
timeout_seconds: 45
-->
<!-- Timeout for above service must be more than sleep + timeout for the client-->
```sh
cd producer-app/
../../mvnw -Dspring-boot.run.arguments="--reuse=true" spring-boot:test-run
```
<!-- END_STEP -->
This will start the `producer-app` with Dapr services and the infrastructure needed by the application to run,
in this case RabbitMQ and PostgreSQL. The `producer-app` starts on port `8080` by default.
The `-Dspring-boot.run.arguments="--reuse=true"` flag helps the application to connect to an existing shared
infrastructure if it already exists. For development purposes, and to connect both applications we will set the flag
in both. For more details check the `DaprTestContainersConfig.java` classes in both, the `producer-app` and the `consumer-app`.
Then run in a different terminal:
<!-- STEP
name: Run Demo Consumer Service
match_order: none
output_match_mode: substring
expected_stdout_lines:
- 'Started ConsumerApplication'
background: true
expected_return_code: 143
sleep: 30
timeout_seconds: 45
-->
<!-- Timeout for above service must be more than sleep + timeout for the client-->
```sh
cd consumer-app/
../../mvnw -Dspring-boot.run.arguments="--reuse=true" spring-boot:test-run
```
<!-- END_STEP -->
The `consumer-app` starts in port `8081` by default.
## Interacting with the applications
Now that both applications are up you can place an order by sending a POST request to `:8080/orders/`
You can use `curl` to send a POST request to the `producer-app`:
<!-- STEP
name: Send POST request to Producer App
match_order: none
output_match_mode: substring
expected_stdout_lines:
- 'Order Stored and Event Published'
background: true
sleep: 1
timeout_seconds: 2
-->
<!-- Timeout for above service must be more than sleep + timeout for the client-->
```sh
curl -X POST localhost:8080/orders -H 'Content-Type: application/json' -d '{ "item": "the mars volta EP", "amount": 1 }'
```
<!-- END_STEP -->
If you check the `producer-app` logs you should see the following lines:
```bash
...
Storing Order: Order{id='null', item='the mars volta EP', amount=1}
Publishing Order Event: Order{id='d4f8ea15-b774-441e-bcd2-7a4208a80bec', item='the mars volta EP', amount=1}
```
If you check the `consumer-app` logs you should see the following lines, showing that the message
published by the `producer-app` was correctly consumed by the `consumer-app`:
```bash
Order Event Received: Order{id='d4f8ea15-b774-441e-bcd2-7a4208a80bec', item='the mars volta EP', amount=1}
```
Next, you can create a new customer to trigger the customer's tracking workflow:
<!-- STEP
name: Start Customer Workflow
match_order: none
output_match_mode: substring
expected_stdout_lines:
- 'New Workflow Instance created for Customer'
background: true
sleep: 1
timeout_seconds: 2
-->
<!-- Timeout for above service must be more than sleep + timeout for the client-->
```sh
curl -X POST localhost:8080/customers -H 'Content-Type: application/json' -d '{ "customerName": "salaboy" }'
```
<!-- END_STEP -->
A new Workflow Instance was created to track the customers interactions. Now, the workflow instance
is waiting for the customer to request a follow-up.
You should see in the `producer-app` logs:
```bash
Workflow instance <Workflow Instance Id> started
Let's register the customer: salaboy
Customer: salaboy registered.
Let's wait for the customer: salaboy to request a follow up.
```
Send an event simulating the customer request for a follow-up:
<!-- STEP
name: Emit Customer Follow-up event
match_order: none
output_match_mode: substring
expected_stdout_lines:
- 'Customer Follow-up requested'
background: true
sleep: 1
timeout_seconds: 5
-->
<!-- Timeout for above service must be more than sleep + timeout for the client-->
```sh
curl -X POST localhost:8080/customers/followup -H 'Content-Type: application/json' -d '{ "customerName": "salaboy" }'
```
<!-- END_STEP -->
In the `producer-app` logs you should see that the workflow instance id moved forward to the Customer Follow Up activity:
```bash
Customer follow-up requested: salaboy
Let's book a follow up for the customer: salaboy
Customer: salaboy follow-up done.
Congratulations the customer: salaboy is happy!
```
## Running on Kubernetes
You can run the same example on a Kubernetes cluster. [Check the Kubernetes tutorial here](kubernetes/README.md).

View File

@ -0,0 +1,99 @@
<?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>io.dapr</groupId>
<artifactId>spring-boot-examples</artifactId>
<version>0.14.0-SNAPSHOT</version>
</parent>
<artifactId>consumer-app</artifactId>
<name>consumer-app</name>
<description>Spring Boot, Testcontainers and Dapr Integration Examples :: Consumer App</description>
<properties>
<springboot.version>3.2.6</springboot.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${springboot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-boot-starter</artifactId>
<version>${dapr-java-sdk.alpha-version}</version>
</dependency>
<dependency>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-boot-starter</artifactId>
<version>${dapr.sdk.alpha.version}</version>
</dependency>
<dependency>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-boot-starter-test</artifactId>
<version>${dapr.sdk.alpha.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.20.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>rabbitmq</artifactId>
<version>1.20.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>kafka</artifactId>
<version>1.20.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,26 @@
/*
* Copyright 2025 The Dapr 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
* http://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 io.dapr.springboot.examples.consumer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}

View File

@ -0,0 +1,65 @@
/*
* Copyright 2025 The Dapr 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
* http://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 io.dapr.springboot.examples.consumer;
public class Order {
private String id;
private String item;
private Integer amount;
public Order() {
}
/**
* Creates a new Order.
*
* @param id order id
* @param item item reference
* @param amount of items in the order
*/
public Order(String id, String item, Integer amount) {
this.id = id;
this.item = item;
this.amount = amount;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getItem() {
return item;
}
public void setItem(String item) {
this.item = item;
}
public Integer getAmount() {
return amount;
}
public void setAmount(Integer amount) {
this.amount = amount;
}
@Override
public String toString() {
return "Order{" + "id='" + id + '\'' + ", item='" + item + '\'' + ", amount=" + amount + '}';
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright 2025 The Dapr 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
* http://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 io.dapr.springboot.examples.consumer;
import io.dapr.Topic;
import io.dapr.client.domain.CloudEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
@RestController
public class SubscriberRestController {
private final Logger logger = LoggerFactory.getLogger(SubscriberRestController.class);
private List<CloudEvent> events = new ArrayList<>();
/**
* Subscribe to cloud events.
* @param cloudEvent payload
*/
@PostMapping("subscribe")
@Topic(pubsubName = "pubsub", name = "topic")
public void subscribe(@RequestBody CloudEvent<Order> cloudEvent) {
logger.info("Order Event Received: " + cloudEvent.getData());
events.add(cloudEvent);
}
@GetMapping("events")
public List<CloudEvent> getAllEvents() {
return events;
}
}

View File

@ -0,0 +1,4 @@
dapr.pubsub.name=pubsub
spring.application.name=consumer-app
server.port=8081

View File

@ -0,0 +1,31 @@
/*
* Copyright 2025 The Dapr 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
* http://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 io.dapr.springboot.examples.consumer;
import io.dapr.client.DaprClient;
import io.dapr.spring.boot.autoconfigure.pubsub.DaprPubSubProperties;
import io.dapr.spring.messaging.DaprMessagingTemplate;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties({DaprPubSubProperties.class})
public class ConsumerAppTestConfiguration {
@Bean
public DaprMessagingTemplate<Order> messagingTemplate(DaprClient daprClient,
DaprPubSubProperties daprPubSubProperties) {
return new DaprMessagingTemplate<>(daprClient, daprPubSubProperties.getName(), false);
}
}

View File

@ -0,0 +1,84 @@
/*
* Copyright 2025 The Dapr 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
* http://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 io.dapr.springboot.examples.consumer;
import io.dapr.client.DaprClient;
import io.dapr.spring.messaging.DaprMessagingTemplate;
import io.dapr.springboot.DaprAutoConfiguration;
import io.dapr.testcontainers.DaprContainer;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.testcontainers.containers.wait.strategy.Wait;
import java.io.IOException;
import java.time.Duration;
import static io.restassured.RestAssured.given;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.CoreMatchers.equalTo;
@SpringBootTest(classes = {TestConsumerApplication.class, DaprTestContainersConfig.class,
ConsumerAppTestConfiguration.class, DaprAutoConfiguration.class},
webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
class ConsumerAppTests {
private static final String SUBSCRIPTION_MESSAGE_PATTERN = ".*app is subscribed to the following topics.*";
@Autowired
private DaprMessagingTemplate<Order> messagingTemplate;
@Autowired
private SubscriberRestController subscriberRestController;
@Autowired
private DaprClient daprClient;
@Autowired
private DaprContainer daprContainer;
@BeforeAll
public static void setup() {
org.testcontainers.Testcontainers.exposeHostPorts(8081);
}
@BeforeEach
void setUp() {
RestAssured.baseURI = "http://localhost:" + 8081;
Wait.forLogMessage(SUBSCRIPTION_MESSAGE_PATTERN, 1).waitUntilReady(daprContainer);
}
@Test
void testMessageConsumer() throws InterruptedException, IOException {
messagingTemplate.send("topic", new Order("abc-123", "the mars volta LP", 1));
given().contentType(ContentType.JSON)
.when()
.get("/events")
.then()
.statusCode(200);
await().atMost(Duration.ofSeconds(10))
.until(subscriberRestController.getAllEvents()::size, equalTo(1));
}
}

View File

@ -0,0 +1,98 @@
/*
* Copyright 2025 The Dapr 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
* http://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 io.dapr.springboot.examples.consumer;
import io.dapr.testcontainers.Component;
import io.dapr.testcontainers.DaprContainer;
import io.dapr.testcontainers.DaprLogLevel;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.RabbitMQContainer;
import org.testcontainers.utility.DockerImageName;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@TestConfiguration(proxyBeanMethods = false)
public class DaprTestContainersConfig {
@Bean
public Network getDaprNetwork() {
Network defaultDaprNetwork = new Network() {
@Override
public String getId() {
return "dapr-network";
}
@Override
public void close() {
}
@Override
public Statement apply(Statement base, Description description) {
return null;
}
};
List<com.github.dockerjava.api.model.Network> networks = DockerClientFactory.instance().client().listNetworksCmd()
.withNameFilter("dapr-network").exec();
if (networks.isEmpty()) {
Network.builder().createNetworkCmdModifier(cmd -> cmd.withName("dapr-network")).build().getId();
return defaultDaprNetwork;
} else {
return defaultDaprNetwork;
}
}
@Bean
public RabbitMQContainer rabbitMQContainer(Network daprNetwork, Environment env) {
boolean reuse = env.getProperty("reuse", Boolean.class, false);
return new RabbitMQContainer(DockerImageName.parse("rabbitmq:3.7.25-management-alpine"))
.withExposedPorts(5672)
.withNetworkAliases("rabbitmq")
.withReuse(true)
.withNetwork(daprNetwork);
}
@Bean
@ServiceConnection
public DaprContainer daprContainer(Network daprNetwork, RabbitMQContainer rabbitMQContainer, Environment env) {
boolean reuse = env.getProperty("reuse", Boolean.class, false);
Map<String, String> rabbitMqProperties = new HashMap<>();
rabbitMqProperties.put("connectionString", "amqp://guest:guest@rabbitmq:5672");
rabbitMqProperties.put("user", "guest");
rabbitMqProperties.put("password", "guest");
return new DaprContainer("daprio/daprd:1.14.4")
.withAppName("consumer-app")
.withNetwork(daprNetwork).withComponent(new Component("pubsub",
"pubsub.rabbitmq", "v1", rabbitMqProperties))
.withDaprLogLevel(DaprLogLevel.INFO)
.withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String()))
.withAppPort(8081).withAppChannelAddress("host.testcontainers.internal")
.withReusablePlacement(reuse)
.withAppHealthCheckPath("/actuator/health")
.dependsOn(rabbitMQContainer);
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright 2025 The Dapr 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
* http://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 io.dapr.springboot.examples.consumer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class TestConsumerApplication {
public static void main(String[] args) {
SpringApplication.from(ConsumerApplication::main)
.with(DaprTestContainersConfig.class)
.run(args);
org.testcontainers.Testcontainers.exposeHostPorts(8081);
}
}

View File

@ -0,0 +1,2 @@
dapr.pubsub.name=pubsub
server.port=8081

View File

@ -0,0 +1,100 @@
# Running this example on Kubernetes
To run this example on Kubernetes, you can use any Kubernetes distribution.
We install Dapr on a Kubernetes cluster and then we will deploy both the `producer-app` and `consumer-app`.
## Creating a cluster and installing Dapr
If you don't have any Kubernetes cluster you can use Kubernetes KIND to create a local cluster. We will create a cluster
with a local container registry, so we can push our container images to it. This is covered in the
[KIND documentation here](https://kind.sigs.k8s.io/docs/user/local-registry/).
```bash
./kind-with-registry.sh
```
Once you have the cluster up and running you can install Dapr:
```bash
helm repo add dapr https://dapr.github.io/helm-charts/
helm repo update
helm upgrade --install dapr dapr/dapr \
--version=1.14.4 \
--namespace dapr-system \
--create-namespace \
--wait
```
## Creating containers using Spring Boot and pushing to local registry
Now that we have our cluster set up with a local container registry, we need to build our `producer-app` and `consumer-app` containers.
For this we will use Spring Boot build it functions to create container images using [Buildpacks](https://buildpacks.io):
From inside the `spring-boot-examples/producer-app` directory you can run the following command to create a container:
```bash
mvn spring-boot:build-image
```
Once we have the container image created, we need to tag and push to the local registry, so the image can be used from our local cluster.
Alternatively, you can push the images to a public registry and update the Kubernetes manifests accordingly.
```bash
docker tag producer-app:0.14.0-SNAPSHOT localhost:5001/sb-producer-app
docker push localhost:5001/sb-producer-app
```
From inside the `spring-boot-examples/consumer-app` directory you can run the following command to create a container:
```bash
mvn spring-boot:build-image
```
Once we have the container image created, we need to tag and push to the local registry, so the image can be used from our local cluster.
Alternatively, you can push the images to a public registry and update the Kubernetes manifests accordingly.
```bash
docker tag consumer-app:0.14.0-SNAPSHOT localhost:5001/sb-consumer-app
docker push localhost:5001/sb-consumer-app
```
Now we are ready to install our application into the cluster.
## Installing and interacting with the application
Now that we have a running Kubernetes cluster, we need to first install the components needed by the application.
In this case RabbitMQ and PostgreSQL. We will use Helm to do so:
Let's start with RabbitMQ:
```bash
helm install rabbitmq oci://registry-1.docker.io/bitnamicharts/rabbitmq --set auth.username=guest --set auth.password=guest --set auth.erlangCookie=ABC
```
Then PostgreSQL:
```bash
helm install postgresql oci://registry-1.docker.io/bitnamicharts/postgresql --set global.postgresql.auth.database=dapr --set global.postgresql.auth.postgresPassword=password
```
Once we have these components up and running we can install the application by running from inside
the `spring-boot-examples/kubernetes/` directory:
```bash
kubectl apply -f .
```
Next you need to use `kubectl port-forward` to be able to send requests to the applications.
```bash
kubectl port-forward svc/producer-app 8080:8080
```
In a different terminals you can check the logs of the `producer-app` and `consumer-app`:
```bash
kubectl logs -f producer-app-<POD_ID>
```
and
```bash
kubectl logs -f consumer-app-<POD_ID>
```

View File

@ -0,0 +1,45 @@
apiVersion: v1
kind: Service
metadata:
labels:
app: consumer-app
name: consumer-app
spec:
type: NodePort
ports:
- name: "consumer-app"
port: 8081
targetPort: 8081
nodePort: 31001
selector:
app: consumer-app
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: consumer-app
name: consumer-app
spec:
replicas: 1
selector:
matchLabels:
app: consumer-app
template:
metadata:
annotations:
dapr.io/app-id: consumer-app
dapr.io/app-port: "8081"
dapr.io/enabled: "true"
labels:
app: consumer-app
spec:
containers:
- image: localhost:5001/sb-consumer-app
name: consumer-app
imagePullPolicy: Always
ports:
- containerPort: 8081
name: consumer-app

View File

@ -0,0 +1,64 @@
#!/bin/sh
set -o errexit
# 1. Create registry container unless it already exists
reg_name='kind-registry'
reg_port='5001'
if [ "$(docker inspect -f '{{.State.Running}}' "${reg_name}" 2>/dev/null || true)" != 'true' ]; then
docker run \
-d --restart=always -p "127.0.0.1:${reg_port}:5000" --network bridge --name "${reg_name}" \
registry:2
fi
# 2. Create kind cluster with containerd registry config dir enabled
# TODO: kind will eventually enable this by default and this patch will
# be unnecessary.
#
# See:
# https://github.com/kubernetes-sigs/kind/issues/2875
# https://github.com/containerd/containerd/blob/main/docs/cri/config.md#registry-configuration
# See: https://github.com/containerd/containerd/blob/main/docs/hosts.md
cat <<EOF | kind create cluster --config=-
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
containerdConfigPatches:
- |-
[plugins."io.containerd.grpc.v1.cri".registry]
config_path = "/etc/containerd/certs.d"
EOF
# 3. Add the registry config to the nodes
#
# This is necessary because localhost resolves to loopback addresses that are
# network-namespace local.
# In other words: localhost in the container is not localhost on the host.
#
# We want a consistent name that works from both ends, so we tell containerd to
# alias localhost:${reg_port} to the registry container when pulling images
REGISTRY_DIR="/etc/containerd/certs.d/localhost:${reg_port}"
for node in $(kind get nodes); do
docker exec "${node}" mkdir -p "${REGISTRY_DIR}"
cat <<EOF | docker exec -i "${node}" cp /dev/stdin "${REGISTRY_DIR}/hosts.toml"
[host."http://${reg_name}:5000"]
EOF
done
# 4. Connect the registry to the cluster network if not already connected
# This allows kind to bootstrap the network but ensures they're on the same network
if [ "$(docker inspect -f='{{json .NetworkSettings.Networks.kind}}' "${reg_name}")" = 'null' ]; then
docker network connect "kind" "${reg_name}"
fi
# 5. Document the local registry
# https://github.com/kubernetes/enhancements/tree/master/keps/sig-cluster-lifecycle/generic/1755-communicating-a-local-registry
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
name: local-registry-hosting
namespace: kube-public
data:
localRegistryHosting.v1: |
host: "localhost:${reg_port}"
help: "https://kind.sigs.k8s.io/docs/user/local-registry/"
EOF

View File

@ -0,0 +1,13 @@
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: kvbinding
spec:
type: bindings.postgresql
version: v1
metadata:
- name: connectionString
value: host=postgresql.default.svc.cluster.local user=postgres password=password port=5432 connect_timeout=10
database=dapr
scopes:
- producer-app

View File

@ -0,0 +1,17 @@
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: kvstore
spec:
type: state.postgresql
version: v1
metadata:
- name: keyPrefix
value: name
- name: actorStateStore
value: 'true'
- name: connectionString
value: host=postgresql.default.svc.cluster.local user=postgres password=password port=5432 connect_timeout=10
database=dapr
scopes:
- producer-app

View File

@ -0,0 +1,45 @@
apiVersion: v1
kind: Service
metadata:
labels:
app: producer-app
name: producer-app
spec:
type: NodePort
ports:
- name: "producer-app"
port: 8080
targetPort: 8080
nodePort: 31000
selector:
app: producer-app
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: producer-app
name: producer-app
spec:
replicas: 1
selector:
matchLabels:
app: producer-app
template:
metadata:
annotations:
dapr.io/app-id: producer-app
dapr.io/app-port: "8080"
dapr.io/enabled: "true"
labels:
app: producer-app
spec:
containers:
- image: localhost:5001/sb-producer-app
name: producer-app
imagePullPolicy: Always
ports:
- containerPort: 8080
name: producer-app

View File

@ -0,0 +1,14 @@
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: pubsub
spec:
type: pubsub.rabbitmq
version: v1
metadata:
- name: connectionString
value: amqp://guest:guest@rabbitmq.default.svc.cluster.local:5672
- name: user
value: guest
- name: password
value: guest

View File

@ -0,0 +1,21 @@
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk-parent</artifactId>
<version>1.14.0-SNAPSHOT</version>
</parent>
<artifactId>spring-boot-examples</artifactId>
<version>0.14.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>producer-app</module>
<module>consumer-app</module>
</modules>
</project>

View File

@ -0,0 +1,85 @@
<?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>io.dapr</groupId>
<artifactId>spring-boot-examples</artifactId>
<version>0.14.0-SNAPSHOT</version>
</parent>
<artifactId>producer-app</artifactId>
<name>producer-app</name>
<description>Spring Boot, Testcontainers and Dapr Integration Examples :: Producer App</description>
<properties>
<springboot.version>3.2.6</springboot.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${springboot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-boot-starter</artifactId>
<version>${dapr.sdk.alpha.version}</version>
</dependency>
<dependency>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-boot-starter-test</artifactId>
<version>${dapr.sdk.alpha.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.20.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>rabbitmq</artifactId>
<version>1.20.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,59 @@
/*
* Copyright 2025 The Dapr 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
* http://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 io.dapr.springboot.examples.producer;
public class Customer {
private String customerName;
private String workflowId;
private boolean inCustomerDB = false;
private boolean followUp = false;
public boolean isFollowUp() {
return followUp;
}
public void setFollowUp(boolean followUp) {
this.followUp = followUp;
}
public boolean isInCustomerDB() {
return inCustomerDB;
}
public void setInCustomerDB(boolean inCustomerDB) {
this.inCustomerDB = inCustomerDB;
}
public String getWorkflowId() {
return workflowId;
}
public void setWorkflowId(String workflowId) {
this.workflowId = workflowId;
}
public String getCustomerName() {
return customerName;
}
public void setCustomerName(String customerName) {
this.customerName = customerName;
}
@Override
public String toString() {
return "Customer [customerName=" + customerName + ", workflowId=" + workflowId + ", inCustomerDB="
+ inCustomerDB + ", followUp=" + followUp + "]";
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright 2025 The Dapr 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
* http://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 io.dapr.springboot.examples.producer;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
@Component
public class CustomerStore {
private Map<String, Customer> customers = new HashMap<>();
public void addCustomer(Customer customer) {
customers.put(customer.getCustomerName(), customer);
}
public Customer getCustomer(String customerName) {
return customers.get(customerName);
}
public Collection<Customer> getCustomers() {
return customers.values();
}
}

View File

@ -0,0 +1,89 @@
/*
* Copyright 2025 The Dapr 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
* http://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 io.dapr.springboot.examples.producer;
import io.dapr.spring.workflows.config.EnableDaprWorkflows;
import io.dapr.springboot.examples.producer.workflow.CustomerWorkflow;
import io.dapr.workflows.client.DaprWorkflowClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
@RestController
@EnableDaprWorkflows
public class CustomersRestController {
private final Logger logger = LoggerFactory.getLogger(CustomersRestController.class);
@Autowired
private DaprWorkflowClient daprWorkflowClient;
@Autowired
private CustomerStore customerStore;
@GetMapping("/")
public String root() {
return "OK";
}
private Map<String, String> customersWorkflows = new HashMap<>();
/**
* Track customer endpoint.
*
* @param customer provided customer to track
* @return confirmation that the workflow instance was created for a given customer
*/
@PostMapping("/customers")
public String trackCustomer(@RequestBody Customer customer) {
String instanceId = daprWorkflowClient.scheduleNewWorkflow(CustomerWorkflow.class, customer);
logger.info("Workflow instance " + instanceId + " started");
customersWorkflows.put(customer.getCustomerName(), instanceId);
return "New Workflow Instance created for Customer: " + customer.getCustomerName();
}
/**
* Request customer follow-up.
* @param customer associated with a workflow instance
* @return confirmation that the follow-up was requested
*/
@PostMapping("/customers/followup")
public String customerNotification(@RequestBody Customer customer) {
logger.info("Customer follow-up requested: " + customer.getCustomerName());
String workflowIdForCustomer = customersWorkflows.get(customer.getCustomerName());
if (workflowIdForCustomer == null || workflowIdForCustomer.isEmpty()) {
return "There is no workflow associated with customer: " + customer.getCustomerName();
} else {
daprWorkflowClient.raiseEvent(workflowIdForCustomer, "CustomerReachOut", customer);
return "Customer Follow-up requested";
}
}
public Collection<Customer> getCustomers() {
return customerStore.getCustomers();
}
}

View File

@ -0,0 +1,56 @@
/*
* Copyright 2025 The Dapr 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
* http://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 io.dapr.springboot.examples.producer;
import org.springframework.data.annotation.Id;
public class Order {
@Id
private String id;
private String item;
private Integer amount;
public Order() {
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getItem() {
return item;
}
public void setItem(String item) {
this.item = item;
}
public Integer getAmount() {
return amount;
}
public void setAmount(Integer amount) {
this.amount = amount;
}
@Override
public String toString() {
return "Order{" + "id='" + id + '\'' + ", item='" + item + '\'' + ", amount=" + amount + '}';
}
}

View File

@ -0,0 +1,25 @@
/*
* Copyright 2025 The Dapr 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
* http://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 io.dapr.springboot.examples.producer;
import org.springframework.data.repository.CrudRepository;
import java.util.List;
public interface OrderRepository extends CrudRepository<Order, String> {
List<Order> findByItem(String item);
List<Order> findByAmount(Integer amount);
}

View File

@ -0,0 +1,71 @@
/*
* Copyright 2025 The Dapr 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
* http://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 io.dapr.springboot.examples.producer;
import io.dapr.spring.data.repository.config.EnableDaprRepositories;
import io.dapr.spring.messaging.DaprMessagingTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@EnableDaprRepositories
public class OrdersRestController {
private final Logger logger = LoggerFactory.getLogger(OrdersRestController.class);
@Autowired
private OrderRepository repository;
@Autowired
private DaprMessagingTemplate<Order> messagingTemplate;
/**
* Store orders from customers.
* @param order from the customer
*
* @return confirmation that the order was stored and the event published
*/
@PostMapping("/orders")
public String storeOrder(@RequestBody Order order) {
logger.info("Storing Order: " + order);
repository.save(order);
logger.info("Publishing Order Event: " + order);
messagingTemplate.send("topic", order);
return "Order Stored and Event Published";
}
@GetMapping("/orders")
public Iterable<Order> getAll() {
return repository.findAll();
}
@GetMapping("/orders/byItem/")
public Iterable<Order> getAllByItem(@RequestParam("item") String item) {
return repository.findByItem(item);
}
@GetMapping("/orders/byAmount/")
public Iterable<Order> getAllByItem(@RequestParam("amount") Integer amount) {
return repository.findByAmount(amount);
}
}

View File

@ -0,0 +1,64 @@
/*
* Copyright 2025 The Dapr 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
* http://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 io.dapr.springboot.examples.producer;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.dapr.client.DaprClient;
import io.dapr.spring.boot.autoconfigure.pubsub.DaprPubSubProperties;
import io.dapr.spring.boot.autoconfigure.statestore.DaprStateStoreProperties;
import io.dapr.spring.data.DaprKeyValueAdapterResolver;
import io.dapr.spring.data.DaprKeyValueTemplate;
import io.dapr.spring.data.KeyValueAdapterResolver;
import io.dapr.spring.messaging.DaprMessagingTemplate;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties({DaprPubSubProperties.class, DaprStateStoreProperties.class})
public class ProducerAppConfiguration {
@Bean
public ObjectMapper mapper() {
return new ObjectMapper();
}
/**
* Produce a KeyValueAdapterResolver for Dapr.
* @param daprClient dapr client
* @param mapper object mapper
* @param daprStatestoreProperties properties to configure state store
* @return KeyValueAdapterResolver
*/
@Bean
public KeyValueAdapterResolver keyValueAdapterResolver(DaprClient daprClient, ObjectMapper mapper,
DaprStateStoreProperties daprStatestoreProperties) {
String storeName = daprStatestoreProperties.getName();
String bindingName = daprStatestoreProperties.getBinding();
return new DaprKeyValueAdapterResolver(daprClient, mapper, storeName, bindingName);
}
@Bean
public DaprKeyValueTemplate daprKeyValueTemplate(KeyValueAdapterResolver keyValueAdapterResolver) {
return new DaprKeyValueTemplate(keyValueAdapterResolver);
}
@Bean
public DaprMessagingTemplate<Order> messagingTemplate(DaprClient daprClient,
DaprPubSubProperties daprPubSubProperties) {
return new DaprMessagingTemplate<>(daprClient, daprPubSubProperties.getName(), false);
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright 2025 The Dapr 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
* http://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 io.dapr.springboot.examples.producer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ProducerApplication {
public static void main(String[] args) {
SpringApplication.run(ProducerApplication.class, args);
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright 2025 The Dapr 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
* http://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 io.dapr.springboot.examples.producer.workflow;
import io.dapr.springboot.examples.producer.Customer;
import io.dapr.springboot.examples.producer.CustomerStore;
import io.dapr.workflows.WorkflowActivity;
import io.dapr.workflows.WorkflowActivityContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component
public class CustomerFollowupActivity implements WorkflowActivity {
private final Logger logger = LoggerFactory.getLogger(CustomerFollowupActivity.class);
private final CustomerStore customerStore;
public CustomerFollowupActivity(CustomerStore customerStore) {
this.customerStore = customerStore;
}
@Override
public Object run(WorkflowActivityContext ctx) {
Customer customer = ctx.getInput(Customer.class);
//Let's get the hydrate the real customer from the CustomerStore
customer = customerStore.getCustomer(customer.getCustomerName());
customer.setFollowUp(true);
customerStore.addCustomer(customer);
logger.info("Customer: " + customer.getCustomerName() + " follow-up done.");
return customer;
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright 2025 The Dapr 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
* http://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 io.dapr.springboot.examples.producer.workflow;
import io.dapr.springboot.examples.producer.Customer;
import io.dapr.workflows.Workflow;
import io.dapr.workflows.WorkflowStub;
import org.springframework.stereotype.Component;
import java.time.Duration;
@Component
public class CustomerWorkflow implements Workflow {
@Override
public WorkflowStub create() {
return ctx -> {
String instanceId = ctx.getInstanceId();
Customer customer = ctx.getInput(Customer.class);
customer.setWorkflowId(instanceId);
ctx.getLogger().info("Let's register the customer: " + customer.getCustomerName());
ctx.callActivity(RegisterCustomerActivity.class.getName(), customer, Customer.class).await();
ctx.getLogger().info("Let's wait for the customer: " + customer.getCustomerName() + " to request a follow up.");
customer = ctx.waitForExternalEvent("CustomerReachOut", Duration.ofMinutes(5), Customer.class).await();
ctx.getLogger().info("Let's book a follow up for the customer: " + customer.getCustomerName());
customer = ctx.callActivity(CustomerFollowupActivity.class.getName(), customer, Customer.class).await();
ctx.getLogger().info("Congratulations the customer: " + customer.getCustomerName() + " is happy!");
ctx.complete(customer);
};
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright 2025 The Dapr 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
* http://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 io.dapr.springboot.examples.producer.workflow;
import io.dapr.springboot.examples.producer.Customer;
import io.dapr.springboot.examples.producer.CustomerStore;
import io.dapr.workflows.WorkflowActivity;
import io.dapr.workflows.WorkflowActivityContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component
public class RegisterCustomerActivity implements WorkflowActivity {
private final Logger logger = LoggerFactory.getLogger(RegisterCustomerActivity.class);
private final CustomerStore customerStore;
public RegisterCustomerActivity(CustomerStore customerStore) {
this.customerStore = customerStore;
}
@Override
public Object run(WorkflowActivityContext ctx) {
Customer customer = ctx.getInput(Customer.class);
customer.setInCustomerDB(true);
logger.info("Customer: " + customer.getCustomerName() + " registered.");
customerStore.addCustomer(customer);
return customer;
}
}

View File

@ -0,0 +1,4 @@
spring.application.name=producer-app
dapr.pubsub.name=pubsub
dapr.statestore.name=kvstore
dapr.statestore.binding=kvbinding

View File

@ -0,0 +1,137 @@
/*
* Copyright 2025 The Dapr 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
* http://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 io.dapr.springboot.examples.producer;
import io.dapr.testcontainers.Component;
import io.dapr.testcontainers.DaprContainer;
import io.dapr.testcontainers.Subscription;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.containers.RabbitMQContainer;
import org.testcontainers.utility.DockerImageName;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@TestConfiguration(proxyBeanMethods = false)
public class DaprTestContainersConfig {
static final String CONNECTION_STRING =
"host=postgres user=postgres password=password port=5432 connect_timeout=10 database=dapr_db_repository";
static final Map<String, String> STATE_STORE_PROPERTIES = createStateStoreProperties();
static final Map<String, String> BINDING_PROPERTIES = Collections.singletonMap("connectionString", CONNECTION_STRING);
@Bean
public Network getNetwork() {
Network defaultDaprNetwork = new Network() {
@Override
public String getId() {
return "dapr-network";
}
@Override
public void close() {
}
@Override
public Statement apply(Statement base, Description description) {
return null;
}
};
List<com.github.dockerjava.api.model.Network> networks = DockerClientFactory.instance().client().listNetworksCmd().withNameFilter("dapr-network").exec();
if (networks.isEmpty()) {
Network.builder()
.createNetworkCmdModifier(cmd -> cmd.withName("dapr-network"))
.build().getId();
return defaultDaprNetwork;
} else {
return defaultDaprNetwork;
}
}
@Bean
public RabbitMQContainer rabbitMQContainer(Network daprNetwork, Environment env) {
boolean reuse = env.getProperty("reuse", Boolean.class, false);
return new RabbitMQContainer(DockerImageName.parse("rabbitmq:3.7.25-management-alpine"))
.withExposedPorts(5672)
.withNetworkAliases("rabbitmq")
.withReuse(true)
.withNetwork(daprNetwork);
}
@Bean
public PostgreSQLContainer<?> postgreSQLContainer(Network daprNetwork) {
return new PostgreSQLContainer<>("postgres:16-alpine")
.withNetworkAliases("postgres")
.withDatabaseName("dapr_db_repository")
.withUsername("postgres")
.withPassword("password")
.withExposedPorts(5432)
.withNetwork(daprNetwork);
}
@Bean
@ServiceConnection
public DaprContainer daprContainer(Network daprNetwork, PostgreSQLContainer<?> postgreSQLContainer, RabbitMQContainer rabbitMQContainer) {
Map<String, String> rabbitMqProperties = new HashMap<>();
rabbitMqProperties.put("connectionString", "amqp://guest:guest@rabbitmq:5672");
rabbitMqProperties.put("user", "guest");
rabbitMqProperties.put("password", "guest");
return new DaprContainer("daprio/daprd:1.14.4")
.withAppName("producer-app")
.withNetwork(daprNetwork)
.withComponent(new Component("kvstore", "state.postgresql", "v1", STATE_STORE_PROPERTIES))
.withComponent(new Component("kvbinding", "bindings.postgresql", "v1", BINDING_PROPERTIES))
.withComponent(new Component("pubsub", "pubsub.rabbitmq", "v1", rabbitMqProperties))
.withSubscription(new Subscription("app", "pubsub", "topic", "/subscribe"))
// .withDaprLogLevel(DaprLogLevel.DEBUG)
// .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String()))
.withAppPort(8080)
.withAppHealthCheckPath("/actuator/health")
.withAppChannelAddress("host.testcontainers.internal")
.dependsOn(rabbitMQContainer)
.dependsOn(postgreSQLContainer);
}
private static Map<String, String> createStateStoreProperties() {
Map<String, String> result = new HashMap<>();
result.put("keyPrefix", "name");
result.put("actorStateStore", String.valueOf(true));
result.put("connectionString", CONNECTION_STRING);
return result;
}
}

View File

@ -0,0 +1,150 @@
/*
* Copyright 2025 The Dapr 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
* http://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 io.dapr.springboot.examples.producer;
import io.dapr.client.DaprClient;
import io.dapr.springboot.DaprAutoConfiguration;
import io.dapr.springboot.examples.producer.workflow.CustomerFollowupActivity;
import io.dapr.springboot.examples.producer.workflow.CustomerWorkflow;
import io.dapr.springboot.examples.producer.workflow.RegisterCustomerActivity;
import io.dapr.testcontainers.DaprContainer;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.testcontainers.containers.wait.strategy.Wait;
import java.io.IOException;
import java.time.Duration;
import static io.restassured.RestAssured.given;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
@SpringBootTest(classes = {TestProducerApplication.class, DaprTestContainersConfig.class,
DaprAutoConfiguration.class, CustomerWorkflow.class, CustomerFollowupActivity.class,
RegisterCustomerActivity.class, CustomerStore.class},
webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
class ProducerAppTests {
private static final String SUBSCRIPTION_MESSAGE_PATTERN = ".*app is subscribed to the following topics.*";
@Autowired
private TestSubscriberRestController controller;
@Autowired
private CustomerStore customerStore;
@Autowired
private DaprClient daprClient;
@Autowired
private DaprContainer daprContainer;
@BeforeEach
void setUp() {
RestAssured.baseURI = "http://localhost:" + 8080;
org.testcontainers.Testcontainers.exposeHostPorts(8080);
// Ensure the subscriptions are registered
Wait.forLogMessage(SUBSCRIPTION_MESSAGE_PATTERN, 1).waitUntilReady(daprContainer);
}
@Test
void testOrdersEndpointAndMessaging() throws InterruptedException, IOException {
given().contentType(ContentType.JSON)
.body("{ \"id\": \"abc-123\",\"item\": \"the mars volta LP\",\"amount\": 1}")
.when()
.post("/orders")
.then()
.statusCode(200);
await().atMost(Duration.ofSeconds(15))
.until(controller.getAllEvents()::size, equalTo(1));
given().contentType(ContentType.JSON)
.when()
.get("/orders")
.then()
.statusCode(200).body("size()", is(1));
given().contentType(ContentType.JSON)
.when()
.queryParam("item", "the mars volta LP")
.get("/orders/byItem/")
.then()
.statusCode(200).body("size()", is(1));
given().contentType(ContentType.JSON)
.when()
.queryParam("item", "other")
.get("/orders/byItem/")
.then()
.statusCode(200).body("size()", is(0));
given().contentType(ContentType.JSON)
.when()
.queryParam("amount", 1)
.get("/orders/byAmount/")
.then()
.statusCode(200).body("size()", is(1));
given().contentType(ContentType.JSON)
.when()
.queryParam("amount", 2)
.get("/orders/byAmount/")
.then()
.statusCode(200).body("size()", is(0));
}
@Test
void testCustomersWorkflows() throws InterruptedException, IOException {
given().contentType(ContentType.JSON)
.body("{\"customerName\": \"salaboy\"}")
.when()
.post("/customers")
.then()
.statusCode(200);
await().atMost(Duration.ofSeconds(15))
.until(customerStore.getCustomers()::size, equalTo(1));
Customer customer = customerStore.getCustomer("salaboy");
assertEquals(true, customer.isInCustomerDB());
String workflowId = customer.getWorkflowId();
given().contentType(ContentType.JSON)
.body("{ \"workflowId\": \"" + workflowId + "\",\"customerName\": \"salaboy\" }")
.when()
.post("/customers/followup")
.then()
.statusCode(200);
assertEquals(1, customerStore.getCustomers().size());
await().atMost(Duration.ofSeconds(10))
.until(customerStore.getCustomer("salaboy")::isFollowUp, equalTo(true));
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright 2025 The Dapr 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
* http://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 io.dapr.springboot.examples.producer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class TestProducerApplication {
public static void main(String[] args) {
SpringApplication.from(ProducerApplication::main)
.with(DaprTestContainersConfig.class)
.run(args);
org.testcontainers.Testcontainers.exposeHostPorts(8080);
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright 2025 The Dapr 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
* http://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 io.dapr.springboot.examples.producer;
import io.dapr.Topic;
import io.dapr.client.domain.CloudEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
@RestController
public class TestSubscriberRestController {
private List<CloudEvent> events = new ArrayList<>();
private final Logger logger = LoggerFactory.getLogger(TestSubscriberRestController.class);
@PostMapping("subscribe")
@Topic(pubsubName = "pubsub", name = "topic")
public void subscribe(@RequestBody CloudEvent<Order> cloudEvent){
logger.info("Order Event Received: " + cloudEvent.getData());
events.add(cloudEvent);
}
public List<CloudEvent> getAllEvents() {
return events;
}
}

View File

@ -0,0 +1,3 @@
dapr.statestore.name=kvstore
dapr.statestore.binding=kvbinding
dapr.pubsub.name=pubsub

View File

@ -0,0 +1,6 @@
<FindBugsFilter>
<!--Ignoring checking for examples-->
<Match>
<Package name="~io\.dapr\.springboot.examples.*"/>
</Match>
</FindBugsFilter>