combined patterns working

Signed-off-by: salaboy <Salaboy@gmail.com>
This commit is contained in:
salaboy 2025-08-12 09:57:11 +01:00
parent 0bdbe01f5d
commit 4bc89c44f4
20 changed files with 182 additions and 74 deletions

View File

@ -46,6 +46,12 @@
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.redis</groupId>
<artifactId>testcontainers-redis</artifactId>
<version>2.2.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>

View File

@ -1,34 +0,0 @@
/*
* 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;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class ShippingAppConfiguration {
@Bean
public RestTemplate restTemplate() {
return new RestTemplateBuilder().build();
}
@Bean
public ObjectMapper mapper() {
return new ObjectMapper();
}
}

View File

@ -14,7 +14,9 @@ limitations under the License.
package io.dapr.springboot.examples;
import io.dapr.Topic;
import io.dapr.client.DaprClient;
import io.dapr.client.domain.CloudEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@ -44,10 +46,11 @@ public class ShippingAppRestController {
// The RegisterShipment activity in the WorkflowApp is publishing to this topic.
// This method is publishing a message to the shipment-registration-confirmed-events topic.
@PostMapping("registerShipment")
public ShipmentRegistrationStatus registerShipment(@RequestBody Order order){
logger.info("registerShipment: Received input: {}", order);
var status = new ShipmentRegistrationStatus(order.id(), true, "");
if( order.id().isEmpty()){
@Topic(pubsubName = DAPR_PUBSUB_COMPONENT, name = DAPR_PUBSUB_REGISTRATION_TOPIC)
public ShipmentRegistrationStatus registerShipment(@RequestBody CloudEvent<Order> order){
logger.info("registerShipment: Received input: {}", order.getData());
var status = new ShipmentRegistrationStatus(order.getData().id(), true, "");
if( order.getData().id().isEmpty()){
logger.info("Order Id is empty!");
}else{
daprClient.publishEvent(DAPR_PUBSUB_COMPONENT,

View File

@ -1 +1,2 @@
spring.application.name=shipping-app
server.port=8081

View File

@ -13,29 +13,96 @@ limitations under the License.
package io.dapr.springboot.examples;
import com.redis.testcontainers.RedisContainer;
import io.dapr.testcontainers.Component;
import io.dapr.testcontainers.DaprContainer;
import io.dapr.testcontainers.DaprLogLevel;
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 java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static io.dapr.springboot.examples.ShippingAppRestController.DAPR_PUBSUB_COMPONENT;
import static io.dapr.springboot.examples.ShippingAppRestController.DAPR_PUBSUB_REGISTRATION_TOPIC;
import static io.dapr.testcontainers.DaprContainerConstants.DAPR_RUNTIME_IMAGE_TAG;
@TestConfiguration(proxyBeanMethods = false)
public class DaprTestContainersConfig {
@Bean
public RedisContainer redisContainer(Network daprNetwork, Environment env){
boolean reuse = env.getProperty("reuse", Boolean.class, false);
return new RedisContainer(RedisContainer.DEFAULT_IMAGE_NAME)
.withNetwork(daprNetwork)
.withReuse(reuse)
.withNetworkAliases("redis");
}
@Bean
@ServiceConnection
public DaprContainer daprContainer() {
public DaprContainer daprContainer(Network daprNetwork, RedisContainer redisContainer, Environment env) {
boolean reuse = env.getProperty("reuse", Boolean.class, false);
Map<String, String> redisProps = new HashMap<>();
redisProps.put("redisHost", "redis:6379");
redisProps.put("redisPassword", "");
return new DaprContainer(DAPR_RUNTIME_IMAGE_TAG)
.withAppName("external-system-interactions")
.withComponent(new Component(DAPR_PUBSUB_COMPONENT, "pubsub.in-memory", "v1", Collections.emptyList()))
.withAppPort(8080)
.withAppName("shipping-app")
.withComponent(new Component(DAPR_PUBSUB_COMPONENT, "pubsub.redis", "v1", redisProps))
.withAppPort(8081)
.withNetwork(daprNetwork)
.withReuseScheduler(reuse)
.withReusablePlacement(reuse)
.withAppHealthCheckPath("/actuator/health")
.withAppChannelAddress("host.testcontainers.internal");
.withAppChannelAddress("host.testcontainers.internal")
// .withDaprLogLevel(DaprLogLevel.DEBUG)
// .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String()))
.dependsOn(redisContainer);
}
@Bean
public Network getDaprNetwork(Environment env) {
boolean reuse = env.getProperty("reuse", Boolean.class, false);
if (reuse) {
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;
}
} else {
return Network.newNetwork();
}
}

View File

@ -40,8 +40,8 @@ class ShippingAppTests {
@BeforeEach
void setUp() {
RestAssured.baseURI = "http://localhost:" + 8080;
org.testcontainers.Testcontainers.exposeHostPorts(8080);
RestAssured.baseURI = "http://localhost:" + 8081;
org.testcontainers.Testcontainers.exposeHostPorts(8081);
}

View File

@ -25,7 +25,7 @@ public class TestShippingApplication {
SpringApplication.from(ShippingApplication::main)
.with(DaprTestContainersConfig.class)
.run(args);
org.testcontainers.Testcontainers.exposeHostPorts(8080);
org.testcontainers.Testcontainers.exposeHostPorts(8081);
}
}

View File

@ -1 +1,2 @@
spring.application.name=external-system-interactions
spring.application.name=shipping-app
server.port=8081

View File

@ -5,6 +5,8 @@ import io.dapr.springboot.workflowapp.model.ProductInventory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import static io.dapr.springboot.workflowapp.WorkflowAppRestController.DAPR_INVENTORY_COMPONENT;
@Component
public class InventoryManagementService {
@ -12,8 +14,10 @@ public class InventoryManagementService {
private DaprClient daprClient;
public void createDefaultInventory(){
ProductInventory productInventory = new ProductInventory("RBD001", 50);
daprClient.saveState("inventory", productInventory.productId(), productInventory).block();
ProductInventoryItem productInventory = new ProductInventoryItem("RBD001", "Rubber Duck",50);
daprClient.saveState(DAPR_INVENTORY_COMPONENT, productInventory.productId(), productInventory).block();
}
record ProductInventoryItem(String productId, String productName, int quantity){}
}

View File

@ -14,14 +14,18 @@ limitations under the License.
package io.dapr.springboot.workflowapp;
import io.dapr.Topic;
import io.dapr.client.DaprClient;
import io.dapr.client.domain.CloudEvent;
import io.dapr.client.domain.State;
import io.dapr.spring.workflows.config.EnableDaprWorkflows;
import io.dapr.springboot.workflowapp.model.Order;
import io.dapr.springboot.workflowapp.model.OrderStatus;
import io.dapr.springboot.workflowapp.model.ProductInventory;
import io.dapr.springboot.workflowapp.model.ShipmentRegistrationStatus;
import io.dapr.springboot.workflowapp.workflow.OrderWorkflow;
import io.dapr.workflows.client.DaprWorkflowClient;
import io.dapr.workflows.client.WorkflowInstanceStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@ -69,7 +73,7 @@ public class WorkflowAppRestController {
@PostMapping("start")
public String basic(@RequestBody Order order) throws TimeoutException {
logger.info("Received order: {}", order);
return daprWorkflowClient.scheduleNewWorkflow(OrderWorkflow.class, order);
return daprWorkflowClient.scheduleNewWorkflow(OrderWorkflow.class, order, order.id());
}
@ -80,9 +84,10 @@ public class WorkflowAppRestController {
* @param status ShipmentRegistrationStatus
*/
@PostMapping("shipmentRegistered")
public void shipmentRegistered(@RequestBody ShipmentRegistrationStatus status){
logger.info("Shipment registered for order {}", status);
daprWorkflowClient.raiseEvent(status.orderId(), SHIPMENT_REGISTERED_EVENT, status);
@Topic(pubsubName = DAPR_PUBSUB_COMPONENT, name = "shipment-registration-confirmed-events")
public void shipmentRegistered(@RequestBody CloudEvent<ShipmentRegistrationStatus> status){
logger.info("Shipment registered for order {}", status.getData());
daprWorkflowClient.raiseEvent(status.getData().orderId(), SHIPMENT_REGISTERED_EVENT, status.getData());
}
@ -114,6 +119,13 @@ public class WorkflowAppRestController {
}
}
@GetMapping("/output")
public OrderStatus getOutput(@RequestParam("instanceId") String instanceId){
WorkflowInstanceStatus instanceState = daprWorkflowClient.getInstanceState(instanceId, true);
assert instanceState != null;
return instanceState.readOutputAs(OrderStatus.class);
}

View File

@ -24,6 +24,8 @@ import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.List;
import static io.dapr.springboot.workflowapp.WorkflowAppRestController.SHIPMENT_REGISTERED_EVENT;
@Component
public class OrderWorkflow implements Workflow {
@Override
@ -61,11 +63,11 @@ public class OrderWorkflow implements Workflow {
try
{
// The RegisterShipment activity is using pub/sub messaging to communicate with the ShippingApp.
ctx.callActivity(RegisterShipmentActivity.class.getName(), order.orderItem(), RegisterShipmentResult.class).await();
ctx.callActivity(RegisterShipmentActivity.class.getName(), order, RegisterShipmentResult.class).await();
// The ShippingApp will also use pub/sub messaging back to the WorkflowApp and raise an event.
// The workflow will wait for the event to be received or until the timeout occurs.
shipmentRegistrationStatus = ctx.waitForExternalEvent("", Duration.ofSeconds(300), ShipmentRegistrationStatus.class).await();
shipmentRegistrationStatus = ctx.waitForExternalEvent(SHIPMENT_REGISTERED_EVENT, Duration.ofSeconds(30), ShipmentRegistrationStatus.class).await();
} catch (TaskCanceledException tce){
// Timeout occurred, the shipment-registered-event was not received.
var message = "ShipmentRegistrationStatus for " + order.id() + " timed out.";

View File

@ -24,6 +24,8 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import static io.dapr.springboot.workflowapp.WorkflowAppRestController.DAPR_INVENTORY_COMPONENT;
@Component
public class CheckInventoryActivity implements WorkflowActivity {
@ -36,7 +38,7 @@ public class CheckInventoryActivity implements WorkflowActivity {
var orderItem = ctx.getInput(OrderItem.class);
logger.info("{} : Received input: {}", ctx.getName(), orderItem);
var productInventoryState = daprClient.getState("inventory", orderItem.productId(), ProductInventory.class).block();
var productInventoryState = daprClient.getState(DAPR_INVENTORY_COMPONENT, orderItem.productId(), ProductInventory.class).block();
assert productInventoryState != null;
ProductInventory productInventory = productInventoryState.getValue();
if (productInventory == null)

View File

@ -19,7 +19,12 @@ import io.dapr.workflows.WorkflowActivity;
import io.dapr.workflows.WorkflowActivityContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
/*
@ -31,12 +36,27 @@ public class CheckShippingDestinationActivity implements WorkflowActivity {
*/
@Autowired
private RestTemplate restTemplate;
@Override
public Object run(WorkflowActivityContext ctx) {
Logger logger = LoggerFactory.getLogger(CheckShippingDestinationActivity.class);
var order = ctx.getInput(Order.class);
// Imagine the order being processed by another system
logger.info("{} : Processed Order: {}", ctx.getName(), order.id());
return new ActivityResult(true, "");
logger.info("{} : Checking Shipping Destination for Order: {}", ctx.getName(), order.id());
HttpEntity<Order> orderHttpEntity = new HttpEntity<>(order);
String url = "http://localhost:8081/checkDestination"; // <- Shipping app URL
ResponseEntity<ShippingDestinationResult> httpPost = restTemplate.exchange(url, HttpMethod.POST,
orderHttpEntity, ShippingDestinationResult.class);
if(!httpPost.getStatusCode().is2xxSuccessful()){
logger.info("{} : Failed to register shipment. Reason:: {}", ctx.getName(), httpPost.getStatusCode().value());
throw new RuntimeException("Failed to register shipment. Reason: " + httpPost.getStatusCode().value());
}
ShippingDestinationResult result = httpPost.getBody();
assert result != null;
return new ActivityResult(result.isSuccess(), "");
}
record ShippingDestinationResult(boolean isSuccess){}
}

View File

@ -13,6 +13,8 @@ limitations under the License.
package io.dapr.springboot.workflowapp.workflow.activities;
import io.dapr.springboot.workflowapp.model.Order;
import io.dapr.springboot.workflowapp.model.OrderItem;
import io.dapr.springboot.workflowapp.model.PaymentResult;
import io.dapr.workflows.WorkflowActivity;
import io.dapr.workflows.WorkflowActivityContext;
@ -32,10 +34,9 @@ public class ProcessPaymentActivity implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
Logger logger = LoggerFactory.getLogger(ProcessPaymentActivity.class);
var message = ctx.getInput(String.class);
var orderItem = ctx.getInput(OrderItem.class);
// Imagine a notification being sent to the user
logger.info("{} : Sending Notification: {}", ctx.getName(), message);
logger.info("{} : Process Order Item Payment: {}", ctx.getName(), orderItem);
return new PaymentResult(true);
}
}

View File

@ -13,22 +13,36 @@ limitations under the License.
package io.dapr.springboot.workflowapp.workflow.activities;
import io.dapr.client.DaprClient;
import io.dapr.springboot.workflowapp.model.Order;
import io.dapr.springboot.workflowapp.model.OrderItem;
import io.dapr.springboot.workflowapp.model.RegisterShipmentResult;
import io.dapr.workflows.WorkflowActivity;
import io.dapr.workflows.WorkflowActivityContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import static io.dapr.springboot.workflowapp.WorkflowAppRestController.DAPR_PUBSUB_COMPONENT;
import static io.dapr.springboot.workflowapp.WorkflowAppRestController.DAPR_PUBSUB_REGISTRATION_TOPIC;
@Component
public class RegisterShipmentActivity implements WorkflowActivity {
@Autowired
private DaprClient daprClient;
@Override
public Object run(WorkflowActivityContext ctx) {
Logger logger = LoggerFactory.getLogger(RegisterShipmentActivity.class);
var order = ctx.getInput(Order.class);
logger.info("{} : RegisterShipmentActivity for Order: {}", ctx.getName(), order.id());
logger.info("{} : RegisterShipmentActivity for OrderItem: {}", ctx.getName(), order);
daprClient.publishEvent(DAPR_PUBSUB_COMPONENT, DAPR_PUBSUB_REGISTRATION_TOPIC, order)
.block();
return new RegisterShipmentResult(true);
}
}

View File

@ -28,7 +28,7 @@ public class ReimburseCustomerActivity implements WorkflowActivity {
public Object run(WorkflowActivityContext ctx) {
Logger logger = LoggerFactory.getLogger(ReimburseCustomerActivity.class);
var order = ctx.getInput(Order.class);
logger.info("{} : Request Approval for Order: {}", ctx.getName(), order.id());
logger.info("{} : Request reimbursement for Order: {}", ctx.getName(), order.id());
return new ReimburseCustomerResult(true);
}
}

View File

@ -26,6 +26,8 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import static io.dapr.springboot.workflowapp.WorkflowAppRestController.DAPR_INVENTORY_COMPONENT;
@Component
public class UpdateInventoryActivity implements WorkflowActivity {
@ -50,7 +52,7 @@ public class UpdateInventoryActivity implements WorkflowActivity {
}
var updateProductInventory = new ProductInventory(productInventory.productId(), productInventory.quantity() - orderItem.quantity());
daprClient.saveState("inventory", orderItem.productId(), updateProductInventory).block();
daprClient.saveState(DAPR_INVENTORY_COMPONENT, orderItem.productId(), updateProductInventory).block();
return new UpdateInventoryResult(true, "Inventory updated for: " + orderItem.productName());
}

View File

@ -1,2 +1,2 @@
spring.application.name=workflow-app
dapr.client.state=
server.port=8080

View File

@ -17,6 +17,7 @@ import com.redis.testcontainers.RedisContainer;
import io.dapr.testcontainers.Component;
import io.dapr.testcontainers.DaprContainer;
import io.dapr.testcontainers.DaprLogLevel;
import io.dapr.testcontainers.Subscription;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.springframework.boot.test.context.TestConfiguration;
@ -31,38 +32,43 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static io.dapr.springboot.workflowapp.WorkflowAppRestController.DAPR_PUBSUB_COMPONENT;
import static io.dapr.testcontainers.DaprContainerConstants.DAPR_RUNTIME_IMAGE_TAG;
@TestConfiguration(proxyBeanMethods = false)
public class DaprTestContainersConfig {
@Bean
public RedisContainer redisContainer(Network daprNetwork){
public RedisContainer redisContainer(Network daprNetwork, Environment env){
boolean reuse = env.getProperty("reuse", Boolean.class, false);
return new RedisContainer(RedisContainer.DEFAULT_IMAGE_NAME)
.withNetwork(daprNetwork)
.withReuse(reuse)
.withNetworkAliases("redis");
}
@Bean
@ServiceConnection
public DaprContainer daprContainer(Network daprNetwork, RedisContainer redisContainer) {
public DaprContainer daprContainer(Network daprNetwork, RedisContainer redisContainer, Environment env) {
boolean reuse = env.getProperty("reuse", Boolean.class, false);
Map<String, String> redisProps = new HashMap<>();
redisProps.put("actorStateStore", String.valueOf(true));
redisProps.put("redisHost", "redis:6379");
redisProps.put("redisPassword", "");
return new DaprContainer(DAPR_RUNTIME_IMAGE_TAG)
.withAppName("workflow-app")
.withComponent(new Component("inventory", "state.redis", "v1", redisProps))
.withComponent(new Component("pubsub", "pubsub.redis", "v1", redisProps))
.withComponent(new Component("inventory", "state.in-memory", "v1",
Collections.singletonMap("actorStateStore", String.valueOf(true))))
.withComponent(new Component( DAPR_PUBSUB_COMPONENT, "pubsub.redis", "v1", redisProps))
.withAppPort(8080)
.withNetwork(daprNetwork)
.withReusablePlacement(reuse)
.withReuseScheduler(reuse)
.withAppHealthCheckPath("/actuator/health")
.withAppChannelAddress("host.testcontainers.internal")
//.withDaprLogLevel(DaprLogLevel.DEBUG)
//.withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String()))
// .withDaprLogLevel(DaprLogLevel.DEBUG)
// .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String()))
.dependsOn(redisContainer);
}

View File

@ -1 +1,2 @@
spring.application.name=workflow-app
server.port=8080