mirror of https://github.com/dapr/java-sdk.git
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:
parent
59abd5dbcc
commit
5dbeafc24a
|
|
@ -27,4 +27,7 @@ mvn versions:set -DnewVersion=$DAPR_JAVA_SDK_ALPHA_VERSION -f testcontainers-dap
|
||||||
# dapr-spring
|
# dapr-spring
|
||||||
mvn versions:set -DnewVersion=$DAPR_JAVA_SDK_ALPHA_VERSION -f dapr-spring/pom.xml
|
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
|
git clean -f
|
||||||
|
|
|
||||||
|
|
@ -164,3 +164,7 @@ jobs:
|
||||||
working-directory: ./examples
|
working-directory: ./examples
|
||||||
run: |
|
run: |
|
||||||
mm.py ./src/main/java/io/dapr/examples/pubsub/stream/README.md
|
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
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ This is the Dapr SDK for Java, including the following features:
|
||||||
* Binding
|
* Binding
|
||||||
* State Store
|
* State Store
|
||||||
* Actors
|
* Actors
|
||||||
|
* Workflows
|
||||||
|
|
||||||
## Getting Started
|
## 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)
|
* [Exception handling](./examples/src/main/java/io/dapr/examples/exception)
|
||||||
* [Unit testing](./examples/src/main/java/io/dapr/examples/unittesting)
|
* [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
|
### API Documentation
|
||||||
|
|
||||||
Please, refer to our [Javadoc](https://dapr.github.io/java-sdk/) website.
|
Please, refer to our [Javadoc](https://dapr.github.io/java-sdk/) website.
|
||||||
|
|
|
||||||
1
pom.xml
1
pom.xml
|
|
@ -337,6 +337,7 @@
|
||||||
<module>sdk-springboot</module>
|
<module>sdk-springboot</module>
|
||||||
<module>dapr-spring</module>
|
<module>dapr-spring</module>
|
||||||
<module>examples</module>
|
<module>examples</module>
|
||||||
|
<module>spring-boot-examples</module>
|
||||||
<!-- We are following test containers artifact convention on purpose, don't rename -->
|
<!-- We are following test containers artifact convention on purpose, don't rename -->
|
||||||
<module>testcontainers-dapr</module>
|
<module>testcontainers-dapr</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
|
||||||
|
|
@ -63,9 +63,10 @@ public class DaprContainerIT {
|
||||||
|
|
||||||
@Container
|
@Container
|
||||||
private static final DaprContainer DAPR_CONTAINER = new DaprContainer(IMAGE_TAG)
|
private static final DaprContainer DAPR_CONTAINER = new DaprContainer(IMAGE_TAG)
|
||||||
.withAppName("dapr-app")
|
.withAppName("dapr-app")
|
||||||
.withAppPort(8081)
|
.withAppPort(8081)
|
||||||
.withAppChannelAddress("host.testcontainers.internal");
|
.withAppHealthCheckPath("/actuator/health")
|
||||||
|
.withAppChannelAddress("host.testcontainers.internal");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the Dapr properties for the test.
|
* Sets the Dapr properties for the test.
|
||||||
|
|
@ -77,18 +78,21 @@ public class DaprContainerIT {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void configStub() {
|
private void configStub() {
|
||||||
|
stubFor(any(urlMatching("/actuator/health"))
|
||||||
|
.willReturn(aResponse().withBody("[]").withStatus(200)));
|
||||||
|
|
||||||
stubFor(any(urlMatching("/dapr/subscribe"))
|
stubFor(any(urlMatching("/dapr/subscribe"))
|
||||||
.willReturn(aResponse().withBody("[]").withStatus(200)));
|
.willReturn(aResponse().withBody("[]").withStatus(200)));
|
||||||
|
|
||||||
stubFor(get(urlMatching("/dapr/config"))
|
stubFor(get(urlMatching("/dapr/config"))
|
||||||
.willReturn(aResponse().withBody("[]").withStatus(200)));
|
.willReturn(aResponse().withBody("[]").withStatus(200)));
|
||||||
|
|
||||||
stubFor(any(urlMatching("/([a-z1-9]*)"))
|
stubFor(any(urlMatching("/([a-z1-9]*)"))
|
||||||
.willReturn(aResponse().withBody("[]").withStatus(200)));
|
.willReturn(aResponse().withBody("[]").withStatus(200)));
|
||||||
|
|
||||||
// create a stub
|
// create a stub
|
||||||
stubFor(post(urlEqualTo("/events"))
|
stubFor(post(urlEqualTo("/events"))
|
||||||
.willReturn(aResponse().withBody("event received!").withStatus(200)));
|
.willReturn(aResponse().withBody("event received!").withStatus(200)));
|
||||||
|
|
||||||
configureFor("localhost", 8081);
|
configureFor("localhost", 8081);
|
||||||
}
|
}
|
||||||
|
|
@ -96,13 +100,13 @@ public class DaprContainerIT {
|
||||||
@Test
|
@Test
|
||||||
public void testDaprContainerDefaults() {
|
public void testDaprContainerDefaults() {
|
||||||
assertEquals(2,
|
assertEquals(2,
|
||||||
DAPR_CONTAINER.getComponents().size(),
|
DAPR_CONTAINER.getComponents().size(),
|
||||||
"The pubsub and kvstore component should be configured by default"
|
"The pubsub and kvstore component should be configured by default"
|
||||||
);
|
);
|
||||||
assertEquals(
|
assertEquals(
|
||||||
1,
|
1,
|
||||||
DAPR_CONTAINER.getSubscriptions().size(),
|
DAPR_CONTAINER.getSubscriptions().size(),
|
||||||
"A subscription should be configured by default if none is provided"
|
"A subscription should be configured by default if none is provided"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -129,10 +133,10 @@ public class DaprContainerIT {
|
||||||
Thread.sleep(1000);
|
Thread.sleep(1000);
|
||||||
|
|
||||||
OkHttpClient okHttpClient = new OkHttpClient.Builder()
|
OkHttpClient okHttpClient = new OkHttpClient.Builder()
|
||||||
.build();
|
.build();
|
||||||
Request request = new Request.Builder()
|
Request request = new Request.Builder()
|
||||||
.url(DAPR_CONTAINER.getHttpEndpoint() + "/v1.0/metadata")
|
.url(DAPR_CONTAINER.getHttpEndpoint() + "/v1.0/metadata")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
try (Response response = okHttpClient.newCall(request).execute()) {
|
try (Response response = okHttpClient.newCall(request).execute()) {
|
||||||
if (response.isSuccessful() && response.body() != null) {
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
|
|
@ -158,7 +162,7 @@ public class DaprContainerIT {
|
||||||
|
|
||||||
private DaprClientBuilder createDaprClientBuilder() {
|
private DaprClientBuilder createDaprClientBuilder() {
|
||||||
return new DaprClientBuilder()
|
return new DaprClientBuilder()
|
||||||
.withPropertyOverride(Properties.HTTP_ENDPOINT, DAPR_CONTAINER.getHttpEndpoint())
|
.withPropertyOverride(Properties.HTTP_ENDPOINT, DAPR_CONTAINER.getHttpEndpoint())
|
||||||
.withPropertyOverride(Properties.GRPC_ENDPOINT, DAPR_CONTAINER.getGrpcEndpoint());
|
.withPropertyOverride(Properties.GRPC_ENDPOINT, DAPR_CONTAINER.getGrpcEndpoint());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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).
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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 + '}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
dapr.pubsub.name=pubsub
|
||||||
|
spring.application.name=consumer-app
|
||||||
|
server.port=8081
|
||||||
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
dapr.pubsub.name=pubsub
|
||||||
|
server.port=8081
|
||||||
|
|
@ -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>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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 + "]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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 + '}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
spring.application.name=producer-app
|
||||||
|
dapr.pubsub.name=pubsub
|
||||||
|
dapr.statestore.name=kvstore
|
||||||
|
dapr.statestore.binding=kvbinding
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
dapr.statestore.name=kvstore
|
||||||
|
dapr.statestore.binding=kvbinding
|
||||||
|
dapr.pubsub.name=pubsub
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<FindBugsFilter>
|
||||||
|
<!--Ignoring checking for examples-->
|
||||||
|
<Match>
|
||||||
|
<Package name="~io\.dapr\.springboot.examples.*"/>
|
||||||
|
</Match>
|
||||||
|
</FindBugsFilter>
|
||||||
Loading…
Reference in New Issue