Skip to content

Commit

Permalink
Adding producer and consumer app examples for Spring Boot integration (
Browse files Browse the repository at this point in the history
…#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>
  • Loading branch information
7 people authored Mar 1, 2025
1 parent 59abd5d commit 5dbeafc
Show file tree
Hide file tree
Showing 43 changed files with 2,047 additions and 17 deletions.
3 changes: 3 additions & 0 deletions .github/scripts/update_sdk_version.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,7 @@ mvn versions:set -DnewVersion=$DAPR_JAVA_SDK_ALPHA_VERSION -f testcontainers-dap
# dapr-spring
mvn versions:set -DnewVersion=$DAPR_JAVA_SDK_ALPHA_VERSION -f dapr-spring/pom.xml

# spring-boot-examples
mvn versions:set -DnewVersion=$DAPR_JAVA_SDK_ALPHA_VERSION -f spring-boot-examples/pom.xml

git clean -f
4 changes: 4 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,7 @@ jobs:
working-directory: ./examples
run: |
mm.py ./src/main/java/io/dapr/examples/pubsub/stream/README.md
- name: Validate Spring Boot examples
working-directory: ./spring-boot-examples
run: |
mm.py README.md
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ This is the Dapr SDK for Java, including the following features:
* Binding
* State Store
* Actors
* Workflows

## Getting Started

Expand Down Expand Up @@ -112,6 +113,13 @@ Try the following examples to learn more about Dapr's Java SDK:
* [Exception handling](./examples/src/main/java/io/dapr/examples/exception)
* [Unit testing](./examples/src/main/java/io/dapr/examples/unittesting)

### Running Spring Boot examples

The Spring Boot integration for Dapr use [Testcontainers](https://testcontainers.com) to set up a local environment development flow that doesn't
require the use of the `dapr` CLI and it integrates with the Spring Boot programming model.

You can find a [step-by-step tutorial showing this integration here](./spring-boot-examples/README.md).

### API Documentation

Please, refer to our [Javadoc](https://dapr.github.io/java-sdk/) website.
Expand Down
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@
<module>sdk-springboot</module>
<module>dapr-spring</module>
<module>examples</module>
<module>spring-boot-examples</module>
<!-- We are following test containers artifact convention on purpose, don't rename -->
<module>testcontainers-dapr</module>
</modules>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,10 @@ public class DaprContainerIT {

@Container
private static final DaprContainer DAPR_CONTAINER = new DaprContainer(IMAGE_TAG)
.withAppName("dapr-app")
.withAppPort(8081)
.withAppChannelAddress("host.testcontainers.internal");
.withAppName("dapr-app")
.withAppPort(8081)
.withAppHealthCheckPath("/actuator/health")
.withAppChannelAddress("host.testcontainers.internal");

/**
* Sets the Dapr properties for the test.
Expand All @@ -77,32 +78,35 @@ public void setDaprProperties() {
}

private void configStub() {
stubFor(any(urlMatching("/actuator/health"))
.willReturn(aResponse().withBody("[]").withStatus(200)));

stubFor(any(urlMatching("/dapr/subscribe"))
.willReturn(aResponse().withBody("[]").withStatus(200)));
.willReturn(aResponse().withBody("[]").withStatus(200)));

stubFor(get(urlMatching("/dapr/config"))
.willReturn(aResponse().withBody("[]").withStatus(200)));
.willReturn(aResponse().withBody("[]").withStatus(200)));

stubFor(any(urlMatching("/([a-z1-9]*)"))
.willReturn(aResponse().withBody("[]").withStatus(200)));
.willReturn(aResponse().withBody("[]").withStatus(200)));

// create a stub
stubFor(post(urlEqualTo("/events"))
.willReturn(aResponse().withBody("event received!").withStatus(200)));
.willReturn(aResponse().withBody("event received!").withStatus(200)));

configureFor("localhost", 8081);
}

@Test
public void testDaprContainerDefaults() {
assertEquals(2,
DAPR_CONTAINER.getComponents().size(),
"The pubsub and kvstore component should be configured by default"
DAPR_CONTAINER.getComponents().size(),
"The pubsub and kvstore component should be configured by default"
);
assertEquals(
1,
DAPR_CONTAINER.getSubscriptions().size(),
"A subscription should be configured by default if none is provided"
1,
DAPR_CONTAINER.getSubscriptions().size(),
"A subscription should be configured by default if none is provided"
);
}

Expand All @@ -129,10 +133,10 @@ public void testPlacement() throws Exception {
Thread.sleep(1000);

OkHttpClient okHttpClient = new OkHttpClient.Builder()
.build();
.build();
Request request = new Request.Builder()
.url(DAPR_CONTAINER.getHttpEndpoint() + "/v1.0/metadata")
.build();
.url(DAPR_CONTAINER.getHttpEndpoint() + "/v1.0/metadata")
.build();

try (Response response = okHttpClient.newCall(request).execute()) {
if (response.isSuccessful() && response.body() != null) {
Expand All @@ -158,7 +162,7 @@ public void testPubSub() throws Exception {

private DaprClientBuilder createDaprClientBuilder() {
return new DaprClientBuilder()
.withPropertyOverride(Properties.HTTP_ENDPOINT, DAPR_CONTAINER.getHttpEndpoint())
.withPropertyOverride(Properties.GRPC_ENDPOINT, DAPR_CONTAINER.getGrpcEndpoint());
.withPropertyOverride(Properties.HTTP_ENDPOINT, DAPR_CONTAINER.getHttpEndpoint())
.withPropertyOverride(Properties.GRPC_ENDPOINT, DAPR_CONTAINER.getGrpcEndpoint());
}
}
176 changes: 176 additions & 0 deletions spring-boot-examples/README.md
Original file line number Diff line number Diff line change
@@ -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).
Loading

0 comments on commit 5dbeafc

Please sign in to comment.