diff --git a/_docs/02-communications.md b/_docs/02-communications.md index be9a7270..41e3fcad 100644 --- a/_docs/02-communications.md +++ b/_docs/02-communications.md @@ -247,14 +247,10 @@ produces and fill in the one method, never touching the wire format itself. {% include codetabs.html langs="Spring Boot|Quarkus|.NET|Python|C++|Go" %} ```java -// inventory-service (excerpt): grpc-java server; protobuf-maven-plugin generates the stubs -@SpringBootApplication -public class InventoryServiceApplication extends InventoryGrpc.InventoryImplBase - implements CommandLineRunner { - @Override public void run(String... args) throws Exception { - server = ServerBuilder.forPort(grpcPort).addService(this).build().start(); - server.awaitTermination(); - } +// inventory-service (excerpt): a @GrpcService bean; spring-boot-starter-grpc-server hosts it on +// spring.grpc.server.port (graceful shutdown, standard health service); protoc generates the stubs +@GrpcService +public class InventoryService extends InventoryGrpc.InventoryImplBase { @Override public void reserveStock(ReserveRequest req, StreamObserver obs) { stock.putIfAbsent(req.getSku(), initialStock); int remaining = stock.get(req.getSku()) - req.getQuantity(); @@ -264,6 +260,10 @@ public class InventoryServiceApplication extends InventoryGrpc.InventoryImplBase obs.onCompleted(); } } +// order-service: the channel comes from spring.grpc.client.channel.inventory.target +@Bean InventoryGrpc.InventoryBlockingStub inventoryStub(GrpcChannelFactory channels) { + return InventoryGrpc.newBlockingStub(channels.createChannel("inventory")); +} ``` ```java diff --git a/_docs/03-composition.md b/_docs/03-composition.md index e8ef7a5d..dfe1495b 100644 --- a/_docs/03-composition.md +++ b/_docs/03-composition.md @@ -64,8 +64,8 @@ the gateway is only the composition layer. @Controller // spring-boot-starter-graphql public class OrderGraphQLController { private final OrderClient orderClient; - private final InventoryGrpcClient inventory; - public OrderGraphQLController(OrderClient o, InventoryGrpcClient i) { + private final InventoryGrpc.InventoryBlockingStub inventory; // Spring gRPC channel bean + public OrderGraphQLController(OrderClient o, InventoryGrpc.InventoryBlockingStub i) { this.orderClient = o; this.inventory = i; } @@ -76,7 +76,8 @@ public class OrderGraphQLController { @SchemaMapping(typeName = "Order", field = "stock") // resolved on demand public Integer stock(Order order) { // via gRPC - return inventory.remaining(order.sku()); + return inventory.getStock(GetStockRequest.newBuilder().setSku(order.sku()).build()) + .getAvailable(); } } ``` diff --git a/_docs/05-event-driven.md b/_docs/05-event-driven.md index e03082fe..7088887f 100644 --- a/_docs/05-event-driven.md +++ b/_docs/05-event-driven.md @@ -576,8 +576,10 @@ the service: ```java // the sidecar (localhost, same pod) owns Kafka — no broker client here -@GrpcClient("sidecar") -private EventBusGrpc.EventBusBlockingStub sidecar; +// Spring gRPC channel: spring.grpc.client.channel.sidecar.target=localhost:50051 +@Bean EventBusGrpc.EventBusBlockingStub sidecar(GrpcChannelFactory channels) { + return EventBusGrpc.newBlockingStub(channels.createChannel("sidecar")); +} void emit(Order o) { sidecar.publish(PublishRequest.newBuilder() diff --git a/_docs/11-observability.md b/_docs/11-observability.md index 67bbd7e2..50844bbf 100644 --- a/_docs/11-observability.md +++ b/_docs/11-observability.md @@ -80,12 +80,13 @@ ENV JAVA_TOOL_OPTIONS="-javaagent:/deployments/opentelemetry-javaagent.jar" // OpenTelemetry API annotation (io.opentelemetry.instrumentation.annotations): @Service public class OrderService { - private final InventoryGrpcClient inventory; - public OrderService(InventoryGrpcClient i) { this.inventory = i; } + private final InventoryServiceGrpc.InventoryServiceBlockingStub inventory; // Spring gRPC + public OrderService(InventoryServiceGrpc.InventoryServiceBlockingStub i) { this.inventory = i; } @WithSpan("reserve_stock") // custom span public ReserveReply reserve(String sku, int qty) { - return inventory.reserveStock(sku, qty); + return inventory.reserveStock( + ReserveRequest.newBuilder().setSku(sku).setQuantity(qty).build()); } // trace_id flows: REST -> gRPC -> Kafka -> consumer } diff --git a/_docs/14-appendix-a-protocols.md b/_docs/14-appendix-a-protocols.md index c8471ab8..78abf84e 100644 --- a/_docs/14-appendix-a-protocols.md +++ b/_docs/14-appendix-a-protocols.md @@ -78,10 +78,10 @@ each language gets a generated client from the one `.proto`. {% include codetabs.html langs="Spring Boot|Quarkus|.NET|Python|C++|Go" %} ```java -// a Spring bean wraps the generated blocking stub (grpc-java, plaintext channel) -@Bean InventoryGrpc.InventoryBlockingStub inventoryStub() { - return InventoryGrpc.newBlockingStub( - ManagedChannelBuilder.forAddress(host, port).usePlaintext().build()); +// a Spring bean wraps the generated blocking stub; Spring gRPC builds and owns the channel +// (spring.grpc.client.channel.inventory.target, plaintext inside the cluster) +@Bean InventoryGrpc.InventoryBlockingStub inventoryStub(GrpcChannelFactory channels) { + return InventoryGrpc.newBlockingStub(channels.createChannel("inventory")); } // constructor-injected: OrderService(InventoryGrpc.InventoryBlockingStub inventory) @@ -180,7 +180,8 @@ note how every one expresses "push many, then finish." {% include codetabs.html langs="Spring Boot|Quarkus|.NET|Python|C++|Go" %} ```java -// server-streaming RPC; registered with ServerBuilder.forPort(port).addService(...) +// server-streaming RPC; Spring gRPC registers the @GrpcService bean on its server +@GrpcService public class OrderEvents extends OrderEventsGrpc.OrderEventsImplBase { @Override public void streamOrderEvents(StreamRequest req, diff --git a/_plans/s14-resilience-plan.md b/_plans/s14-resilience-plan.md index fc0cbc3f..f3ad3b64 100644 --- a/_plans/s14-resilience-plan.md +++ b/_plans/s14-resilience-plan.md @@ -32,7 +32,7 @@ language; do not hand-roll what a maintained library provides. | Spring Boot 4.1.1 | `io.github.resilience4j:resilience4j-spring-boot4` | 2.4.0 (built against Boot 4.0.0) | Maven Central | | Spring Boot 4.1.1 | `spring-boot-starter-aspectj` (the Boot 4 name of the AOP starter) | BOM 4.1.1 | Boot BOM | | Spring Boot 4.1.1 | `spring-boot-starter-grpc-server` / `-client` (Spring gRPC) | 1.1.1 via BOM 4.1.1 | Boot BOM | -| Spring Boot 4.1.1 | `io.github.ascopes:protobuf-maven-plugin` | 5.1.8 via BOM 4.1.1 | Boot BOM | +| Spring Boot 4.1.1 | `io.github.ascopes:protobuf-maven-plugin` | not adopted: fails under the UBI packaged Maven; xolstice 0.6.1 stays | Boot BOM | | Quarkus 3.40.1 | `quarkus-smallrye-fault-tolerance` | BOM 3.40.1 | Quarkus BOM | | .NET 10 | `Microsoft.Extensions.Http.Resilience` | 10.10.0 (Polly.Core 8.8.0) | NuGet | | .NET 10 | `Grpc.Net.ClientFactory` | 2.84.0 (matches Grpc.Net.Client) | NuGet | @@ -120,8 +120,9 @@ example has. Deck slides per language: bulkhead [085] (.NET [087]); Appendix M t - **Spring gRPC:** `spring-boot-starter-grpc-server` in the four inventory services (`@Service` beans extending the generated `ImplBase`, `spring.grpc.server.port=50051`, graceful shutdown by the starter, standard health service), `spring-boot-starter-grpc-client` in the callers (`GrpcChannelFactory` / `@ImportGrpcClients`, - `spring.grpc.client.channels.inventory.address`), the protobuf plugin switched to the Boot-managed - `io.github.ascopes` plugin. 18 keeps its deadline (`withDeadlineAfter`) and its `StatusRuntimeException` mapping. + `spring.grpc.client.channels.inventory.address`), the protobuf plugin stays xolstice 0.6.1: + the Boot-managed `io.github.ascopes` plugin fails to load under the UBI image's packaged Maven (Guice cannot + find ASM), and replacing Maven in every Java image is out of scope. 18 keeps its deadline (`withDeadlineAfter`) and its `StatusRuntimeException` mapping. The OTel agent keeps instrumenting grpc-java; 11's cross-service trace check is the acceptance gate. ## PRs (sequential, each merged green before the next) @@ -144,7 +145,7 @@ Order inside a PR: Spring, Quarkus, .NET, Python, Go, C++, one example at a time | 0 | Preflight: user decisions D1–D4 (done 2026-10-13); pins (done, see Pins); baseline matrix of 12, 26, 02, 03, 11, 18 on main | – | done: baseline 36/36 PASS on main 6656c0a code (2026-10-13) | | 1 | Example 26 + verify.sh + Appendix M prose + decks | 1 | done: 26 × 6 PASS (22/22 each); Appendix M tabs, READMEs, 18 deck slides | | 2 | Example 12 bulkhead + verify.sh + chapter 12 prose + decks | 2 | done: 12 × 6 PASS (18/18 each); chapter 12 tab, READMEs, 6 deck slides | -| 3 | Spring gRPC (+ .NET client factory) + gRPC prose and slide fixes | 3 | pending | +| 3 | Spring gRPC (+ .NET client factory) + gRPC prose and slide fixes | 3 | done: 02, 03, 11, 18 Spring and .NET PASS; gRPC health SERVING; protobuf plugin stays xolstice (see Approach) | | 4 | CLAUDE.md, modernization plan, final 111 matrix, footers | 4 | pending | ## Acceptance criteria diff --git a/examples/02-communications/dotnet/OrderService/OrderService.csproj b/examples/02-communications/dotnet/OrderService/OrderService.csproj index b5723afe..62841717 100644 --- a/examples/02-communications/dotnet/OrderService/OrderService.csproj +++ b/examples/02-communications/dotnet/OrderService/OrderService.csproj @@ -12,7 +12,7 @@ - + diff --git a/examples/02-communications/dotnet/OrderService/Program.cs b/examples/02-communications/dotnet/OrderService/Program.cs index 0c937b3d..92685b0e 100644 --- a/examples/02-communications/dotnet/OrderService/Program.cs +++ b/examples/02-communications/dotnet/OrderService/Program.cs @@ -1,6 +1,5 @@ using System.Text.Json; using Confluent.Kafka; -using Grpc.Net.Client; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.EntityFrameworkCore; using Npgsql; @@ -30,13 +29,12 @@ .AddQueryType(); builder.Services.AddHealthChecks(); +// gRPC client factory: the channel and its HTTP/2 handler are managed like any typed HttpClient. +builder.Services.AddGrpcClient(o => o.Address = new Uri($"http://{inventoryAddr}")) + .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { EnableMultipleHttp2Connections = true }); var app = builder.Build(); -var channel = GrpcChannel.ForAddress($"http://{inventoryAddr}", - new GrpcChannelOptions { HttpHandler = new SocketsHttpHandler { EnableMultipleHttp2Connections = true } }); -var inventoryClient = new Inventory.InventoryClient(channel); - IProducer? producer = null; for (var i = 0; i < 30; i++) { @@ -54,7 +52,7 @@ ResponseWriter = (ctx, _) => ctx.Response.WriteAsJsonAsync(new { status = "ok" }), }); -app.MapPost("/orders", async (HttpContext ctx, OrdersContext db) => +app.MapPost("/orders", async (HttpContext ctx, OrdersContext db, Inventory.InventoryClient inventoryClient) => { using var doc = await JsonDocument.ParseAsync(ctx.Request.Body); var root = doc.RootElement; diff --git a/examples/02-communications/dotnet/README.md b/examples/02-communications/dotnet/README.md index 78a83638..ffb44124 100644 --- a/examples/02-communications/dotnet/README.md +++ b/examples/02-communications/dotnet/README.md @@ -5,7 +5,7 @@ Four communication styles — REST, gRPC, async events (Kafka), and GraphQL — ## Framework & libraries - **Framework**: ASP.NET Core (.NET 10.0.12) minimal API -- **Libraries**: HotChocolate.AspNetCore (GraphQL), Confluent.Kafka 2.16.0, Grpc.Net.Client, Google.Protobuf +- **Libraries**: HotChocolate.AspNetCore (GraphQL), Confluent.Kafka 2.16.0, Grpc.Net.ClientFactory (`AddGrpcClient`), Google.Protobuf - **Container base**: registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 (runtime), built with ubi9/dotnet-100:9.8-1791421718; runs as UID 1001 ## Run @@ -35,7 +35,7 @@ GraphQL via HotChocolate (code-first). gRPC stubs generated via Grpc.Tools at bu Key variables (set by compose.yaml, override for local dev): -`DATABASE_URL, KAFKA_BOOTSTRAP, INVENTORY_GRPC_HOST` +`DATABASE_URL, KAFKA_BOOTSTRAP, INVENTORY_ADDR` ## Local development (without containers) diff --git a/examples/02-communications/spring-boot/README.md b/examples/02-communications/spring-boot/README.md index 67ec66fa..87309ff9 100644 --- a/examples/02-communications/spring-boot/README.md +++ b/examples/02-communications/spring-boot/README.md @@ -5,7 +5,7 @@ Four communication styles — REST, gRPC, async events (Kafka), and GraphQL — ## Framework & libraries - **Framework**: Spring Boot 4.1.1, Java 25 (Web, GraphQL, gRPC) -- **Libraries**: spring-boot-starter-graphql, spring-boot-starter-kafka (Spring Kafka 4), grpc-netty-shaded, grpc-protobuf, OpenTelemetry Java agent 2.32.0 +- **Libraries**: spring-boot-starter-graphql, spring-boot-starter-kafka (Spring Kafka 4), spring-boot-starter-grpc-server (inventory) and spring-boot-starter-grpc-client (callers), Spring gRPC 1.1.1, OpenTelemetry Java agent 2.32.0 - **Container base**: `ubi10/openjdk-25-runtime:1.24-15` (UID 185) ## Run @@ -24,8 +24,8 @@ cd .. && bash verify.sh | Path | Description | |------|-------------| -| `order-service/` | OrderController.java — REST + GraphQL; KafkaProducerConfig — async events; InventoryGrpcClient — gRPC calls | -| `inventory-service/` | InventoryGrpcService.java — gRPC server | +| `order-service/` | OrderController.java — REST + GraphQL; KafkaProducerConfig — async events; inventory stub from Spring gRPC's GrpcChannelFactory — gRPC calls | +| `inventory-service/` | InventoryService.java — `@GrpcService` bean served by Spring gRPC | ## Implementation notes @@ -37,7 +37,7 @@ The gRPC contract lives in `proto/` next to the services. The poms set the proto Key variables (set by compose.yaml, override for local dev): -`spring.datasource.url, spring.kafka.bootstrap-servers, inventory.grpc.host` +`spring.datasource.url, spring.kafka.bootstrap-servers, spring.grpc.client.channel.inventory.target` (from `INVENTORY_ADDR`) ## Local development (without containers) diff --git a/examples/02-communications/spring-boot/inventory-service/pom.xml b/examples/02-communications/spring-boot/inventory-service/pom.xml index 178db7eb..a79771fe 100644 --- a/examples/02-communications/spring-boot/inventory-service/pom.xml +++ b/examples/02-communications/spring-boot/inventory-service/pom.xml @@ -23,23 +23,7 @@ org.springframework.boot - spring-boot-starter - - - io.grpc - grpc-netty-shaded - - - io.grpc - grpc-protobuf - - - io.grpc - grpc-stub - - - com.google.protobuf - protobuf-java + spring-boot-starter-grpc-server diff --git a/examples/02-communications/spring-boot/inventory-service/src/main/java/com/cndp/inventory/InventoryService.java b/examples/02-communications/spring-boot/inventory-service/src/main/java/com/cndp/inventory/InventoryService.java new file mode 100644 index 00000000..b5b301cf --- /dev/null +++ b/examples/02-communications/spring-boot/inventory-service/src/main/java/com/cndp/inventory/InventoryService.java @@ -0,0 +1,46 @@ +package com.cndp.inventory; + +import java.util.concurrent.ConcurrentHashMap; + +import com.cndp.proto.InventoryGrpc; +import com.cndp.proto.ReserveReply; +import com.cndp.proto.ReserveRequest; +import io.grpc.stub.StreamObserver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.grpc.server.service.GrpcService; + +@GrpcService +public class InventoryService extends InventoryGrpc.InventoryImplBase { + + private static final Logger log = LoggerFactory.getLogger(InventoryService.class); + + private final ConcurrentHashMap stock = new ConcurrentHashMap<>(); + private final int initialStock; + + public InventoryService(@Value("${inventory.initial-stock}") int initialStock) { + this.initialStock = initialStock; + } + + @Override + public void reserveStock(ReserveRequest request, StreamObserver responseObserver) { + String sku = request.getSku(); + int qty = request.getQuantity(); + + stock.putIfAbsent(sku, initialStock); + + int remaining = stock.get(sku) - qty; + if (remaining >= 0) { + stock.put(sku, remaining); + log.info("reserved sku={} qty={} remaining={}", sku, qty, remaining); + responseObserver.onNext(ReserveReply.newBuilder() + .setReserved(true).setRemaining(remaining).build()); + } else { + log.warn("insufficient stock sku={} requested={} available={}", sku, qty, stock.get(sku)); + responseObserver.onNext(ReserveReply.newBuilder() + .setReserved(false).setRemaining(stock.get(sku)).build()); + } + responseObserver.onCompleted(); + } +} diff --git a/examples/02-communications/spring-boot/inventory-service/src/main/java/com/cndp/inventory/InventoryServiceApplication.java b/examples/02-communications/spring-boot/inventory-service/src/main/java/com/cndp/inventory/InventoryServiceApplication.java index dd3d166e..e54543cb 100644 --- a/examples/02-communications/spring-boot/inventory-service/src/main/java/com/cndp/inventory/InventoryServiceApplication.java +++ b/examples/02-communications/spring-boot/inventory-service/src/main/java/com/cndp/inventory/InventoryServiceApplication.java @@ -1,73 +1,13 @@ package com.cndp.inventory; -import java.util.concurrent.ConcurrentHashMap; - -import com.cndp.proto.InventoryGrpc; -import com.cndp.proto.ReserveReply; -import com.cndp.proto.ReserveRequest; -import io.grpc.Server; -import io.grpc.ServerBuilder; -import io.grpc.stub.StreamObserver; -import jakarta.annotation.PreDestroy; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +/** Spring gRPC starts the gRPC server (spring.grpc.server.port) and registers every @GrpcService bean. */ @SpringBootApplication -public class InventoryServiceApplication extends InventoryGrpc.InventoryImplBase implements CommandLineRunner { - - private static final Logger log = LoggerFactory.getLogger(InventoryServiceApplication.class); - - private final ConcurrentHashMap stock = new ConcurrentHashMap<>(); - - @Value("${inventory.initial-stock}") - private int initialStock; - - @Value("${grpc.port}") - private int grpcPort; - - private Server server; +public class InventoryServiceApplication { public static void main(String[] args) { SpringApplication.run(InventoryServiceApplication.class, args); } - - @Override - public void run(String... args) throws Exception { - server = ServerBuilder.forPort(grpcPort) - .addService(this) - .build() - .start(); - log.info("inventory gRPC server listening on :{}", grpcPort); - server.awaitTermination(); - } - - @PreDestroy - public void stopGrpcServer() { - if (server != null) server.shutdown(); - } - - @Override - public void reserveStock(ReserveRequest request, StreamObserver responseObserver) { - String sku = request.getSku(); - int qty = request.getQuantity(); - - stock.putIfAbsent(sku, initialStock); - - int remaining = stock.get(sku) - qty; - if (remaining >= 0) { - stock.put(sku, remaining); - log.info("reserved sku={} qty={} remaining={}", sku, qty, remaining); - responseObserver.onNext(ReserveReply.newBuilder() - .setReserved(true).setRemaining(remaining).build()); - } else { - log.warn("insufficient stock sku={} requested={} available={}", sku, qty, stock.get(sku)); - responseObserver.onNext(ReserveReply.newBuilder() - .setReserved(false).setRemaining(stock.get(sku)).build()); - } - responseObserver.onCompleted(); - } } diff --git a/examples/02-communications/spring-boot/inventory-service/src/main/resources/application.yaml b/examples/02-communications/spring-boot/inventory-service/src/main/resources/application.yaml index c726ea25..3476bd7d 100644 --- a/examples/02-communications/spring-boot/inventory-service/src/main/resources/application.yaml +++ b/examples/02-communications/spring-boot/inventory-service/src/main/resources/application.yaml @@ -4,8 +4,8 @@ spring: main: web-application-type: none -grpc: - port: ${GRPC_PORT:50051} +# Spring gRPC: the server port; the standard health and reflection services are on by default. +spring.grpc.server.port: ${GRPC_PORT:50051} inventory: initial-stock: ${INITIAL_STOCK:100} diff --git a/examples/02-communications/spring-boot/order-service/pom.xml b/examples/02-communications/spring-boot/order-service/pom.xml index 69445e83..85f04c46 100644 --- a/examples/02-communications/spring-boot/order-service/pom.xml +++ b/examples/02-communications/spring-boot/order-service/pom.xml @@ -47,20 +47,8 @@ runtime - io.grpc - grpc-netty-shaded - - - io.grpc - grpc-protobuf - - - io.grpc - grpc-stub - - - com.google.protobuf - protobuf-java + org.springframework.boot + spring-boot-starter-grpc-client diff --git a/examples/02-communications/spring-boot/order-service/src/main/java/com/cndp/order/OrderServiceApplication.java b/examples/02-communications/spring-boot/order-service/src/main/java/com/cndp/order/OrderServiceApplication.java index baf42c4d..02d79cf5 100644 --- a/examples/02-communications/spring-boot/order-service/src/main/java/com/cndp/order/OrderServiceApplication.java +++ b/examples/02-communications/spring-boot/order-service/src/main/java/com/cndp/order/OrderServiceApplication.java @@ -1,37 +1,21 @@ package com.cndp.order; import com.cndp.proto.InventoryGrpc; -import io.grpc.ManagedChannel; -import io.grpc.ManagedChannelBuilder; -import jakarta.annotation.PreDestroy; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; +import org.springframework.grpc.client.GrpcChannelFactory; @SpringBootApplication public class OrderServiceApplication { - @Value("${inventory.addr}") - private String inventoryAddr; - - private ManagedChannel channel; - public static void main(String[] args) { SpringApplication.run(OrderServiceApplication.class, args); } + /** The "inventory" channel comes from spring.grpc.client.channel.inventory.target; Spring gRPC owns its lifecycle. */ @Bean - public InventoryGrpc.InventoryBlockingStub inventoryStub() { - String[] parts = inventoryAddr.split(":"); - channel = ManagedChannelBuilder.forAddress(parts[0], Integer.parseInt(parts[1])) - .usePlaintext() - .build(); - return InventoryGrpc.newBlockingStub(channel); - } - - @PreDestroy - public void shutdownChannel() { - if (channel != null) channel.shutdown(); + InventoryGrpc.InventoryBlockingStub inventoryStub(GrpcChannelFactory channels) { + return InventoryGrpc.newBlockingStub(channels.createChannel("inventory")); } } diff --git a/examples/02-communications/spring-boot/order-service/src/main/resources/application.yaml b/examples/02-communications/spring-boot/order-service/src/main/resources/application.yaml index dd615ffa..fa8351b8 100644 --- a/examples/02-communications/spring-boot/order-service/src/main/resources/application.yaml +++ b/examples/02-communications/spring-boot/order-service/src/main/resources/application.yaml @@ -13,8 +13,8 @@ spring: server: port: 8080 -inventory: - addr: ${INVENTORY_ADDR:inventory:50051} +# Spring gRPC client channel (plaintext inside the compose network). +spring.grpc.client.channel.inventory.target: ${INVENTORY_ADDR:inventory:50051} spring.jpa: # Hibernate only checks the entities against db/init; it never changes the schema. diff --git a/examples/03-composition/dotnet/Gateway/Gateway.csproj b/examples/03-composition/dotnet/Gateway/Gateway.csproj index a2f93d26..8adff78e 100644 --- a/examples/03-composition/dotnet/Gateway/Gateway.csproj +++ b/examples/03-composition/dotnet/Gateway/Gateway.csproj @@ -8,7 +8,7 @@ - + diff --git a/examples/03-composition/dotnet/Gateway/Program.cs b/examples/03-composition/dotnet/Gateway/Program.cs index 368948d0..3efa95da 100644 --- a/examples/03-composition/dotnet/Gateway/Program.cs +++ b/examples/03-composition/dotnet/Gateway/Program.cs @@ -1,6 +1,5 @@ using Microsoft.AspNetCore.Diagnostics.HealthChecks; using GreenDonut; -using Grpc.Net.Client; using OpenTelemetry.Resources; using OpenTelemetry.Trace; using Proto; @@ -11,9 +10,9 @@ var orderApiUrl = builder.Configuration.GetValue("ORDER_API_URL", "http://order-api:8081"); var inventoryAddr = builder.Configuration.GetValue("INVENTORY_ADDR", "inventory:50051"); -var channel = GrpcChannel.ForAddress($"http://{inventoryAddr}", - new GrpcChannelOptions { HttpHandler = new SocketsHttpHandler { EnableMultipleHttp2Connections = true } }); -builder.Services.AddSingleton(new Inventory.InventoryClient(channel)); +// gRPC client factory: the channel and its HTTP/2 handler are managed like any typed HttpClient. +builder.Services.AddGrpcClient(o => o.Address = new Uri($"http://{inventoryAddr}")) + .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { EnableMultipleHttp2Connections = true }); builder.Services.AddHttpClient(c => c.BaseAddress = new Uri(orderApiUrl)); builder.Services.AddOpenTelemetry() diff --git a/examples/03-composition/dotnet/README.md b/examples/03-composition/dotnet/README.md index ecaf152e..e6303de6 100644 --- a/examples/03-composition/dotnet/README.md +++ b/examples/03-composition/dotnet/README.md @@ -5,7 +5,7 @@ API gateway pattern — BFF gateway aggregating order-api and inventory-service ## Framework & libraries - **Framework**: ASP.NET Core (.NET 10.0.12) minimal API -- **Libraries**: HotChocolate.AspNetCore (GraphQL), Grpc.Net.Client, Google.Protobuf +- **Libraries**: HotChocolate.AspNetCore (GraphQL), Grpc.Net.ClientFactory (`AddGrpcClient`), Google.Protobuf - **Container base**: registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 (runtime), built with ubi9/dotnet-100:9.8-1791421718; runs as UID 1001 ## Run @@ -36,7 +36,7 @@ Gateway uses HotChocolate code-first GraphQL with resolvers that aggregate from Key variables (set by compose.yaml, override for local dev): -`ORDER_API_URL, INVENTORY_GRPC_HOST` +`ORDER_API_URL, INVENTORY_ADDR` ## Local development (without containers) diff --git a/examples/03-composition/spring-boot/README.md b/examples/03-composition/spring-boot/README.md index d7ea1bb5..cefac03f 100644 --- a/examples/03-composition/spring-boot/README.md +++ b/examples/03-composition/spring-boot/README.md @@ -5,7 +5,7 @@ API gateway pattern — BFF gateway aggregating order-api and inventory-service ## Framework & libraries - **Framework**: Spring Boot 4.1.1, Java 25 (Web, GraphQL, gRPC) -- **Libraries**: spring-boot-starter-graphql, grpc-netty-shaded, grpc-protobuf, OpenTelemetry Java agent 2.32.0 +- **Libraries**: spring-boot-starter-graphql, spring-boot-starter-grpc-server (inventory) and spring-boot-starter-grpc-client (callers), Spring gRPC 1.1.1, OpenTelemetry Java agent 2.32.0 - **Container base**: `ubi10/openjdk-25-runtime:1.24-15` (UID 185) ## Run @@ -26,7 +26,7 @@ cd .. && bash verify.sh |------|-------------| | `gateway/` | GraphQL gateway with Spring @QueryMapping resolvers | | `order-api/` | REST order service | -| `inventory-service/` | gRPC service | +| `inventory-service/` | InventoryService.java — `@GrpcService` bean served by Spring gRPC | ## Implementation notes @@ -38,7 +38,7 @@ The gRPC contract lives in `proto/` next to the services. The poms set the proto Key variables (set by compose.yaml, override for local dev): -`order-api.url, inventory.grpc.host` +`orderapi.url, spring.grpc.client.channel.inventory.target` (from `INVENTORY_ADDR`) ## Local development (without containers) diff --git a/examples/03-composition/spring-boot/gateway/pom.xml b/examples/03-composition/spring-boot/gateway/pom.xml index 3b93ada4..6972b26c 100644 --- a/examples/03-composition/spring-boot/gateway/pom.xml +++ b/examples/03-composition/spring-boot/gateway/pom.xml @@ -34,20 +34,8 @@ spring-boot-starter-graphql - io.grpc - grpc-netty-shaded - - - io.grpc - grpc-protobuf - - - io.grpc - grpc-stub - - - com.google.protobuf - protobuf-java + org.springframework.boot + spring-boot-starter-grpc-client diff --git a/examples/03-composition/spring-boot/gateway/src/main/java/com/cndp/gateway/GatewayApplication.java b/examples/03-composition/spring-boot/gateway/src/main/java/com/cndp/gateway/GatewayApplication.java index 208a6e4d..816250f4 100644 --- a/examples/03-composition/spring-boot/gateway/src/main/java/com/cndp/gateway/GatewayApplication.java +++ b/examples/03-composition/spring-boot/gateway/src/main/java/com/cndp/gateway/GatewayApplication.java @@ -1,37 +1,27 @@ package com.cndp.gateway; import com.cndp.proto.InventoryGrpc; -import io.grpc.ManagedChannel; -import io.grpc.ManagedChannelBuilder; -import jakarta.annotation.PreDestroy; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; +import org.springframework.grpc.client.GrpcChannelFactory; import org.springframework.web.client.RestClient; @SpringBootApplication public class GatewayApplication { - @Value("${inventory.addr}") - private String inventoryAddr; - @Value("${orderapi.url}") private String orderApiUrl; - private ManagedChannel channel; - public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); } + /** The "inventory" channel comes from spring.grpc.client.channel.inventory.target; Spring gRPC owns its lifecycle. */ @Bean - public InventoryGrpc.InventoryBlockingStub inventoryStub() { - String[] parts = inventoryAddr.split(":"); - channel = ManagedChannelBuilder.forAddress(parts[0], Integer.parseInt(parts[1])) - .usePlaintext() - .build(); - return InventoryGrpc.newBlockingStub(channel); + InventoryGrpc.InventoryBlockingStub inventoryStub(GrpcChannelFactory channels) { + return InventoryGrpc.newBlockingStub(channels.createChannel("inventory")); } @Bean @@ -40,9 +30,4 @@ public RestClient orderApiClient() { .baseUrl(orderApiUrl) .build(); } - - @PreDestroy - public void shutdownChannel() { - if (channel != null) channel.shutdown(); - } } diff --git a/examples/03-composition/spring-boot/gateway/src/main/resources/application.yaml b/examples/03-composition/spring-boot/gateway/src/main/resources/application.yaml index 81caf11c..3b841ea4 100644 --- a/examples/03-composition/spring-boot/gateway/src/main/resources/application.yaml +++ b/examples/03-composition/spring-boot/gateway/src/main/resources/application.yaml @@ -8,8 +8,8 @@ spring: server: port: 8080 -inventory: - addr: ${INVENTORY_ADDR:inventory:50051} +# Spring gRPC client channel (plaintext inside the compose network). +spring.grpc.client.channel.inventory.target: ${INVENTORY_ADDR:inventory:50051} orderapi: url: ${ORDER_API_URL:http://order-api:8081} diff --git a/examples/03-composition/spring-boot/inventory-service/pom.xml b/examples/03-composition/spring-boot/inventory-service/pom.xml index 178db7eb..a79771fe 100644 --- a/examples/03-composition/spring-boot/inventory-service/pom.xml +++ b/examples/03-composition/spring-boot/inventory-service/pom.xml @@ -23,23 +23,7 @@ org.springframework.boot - spring-boot-starter - - - io.grpc - grpc-netty-shaded - - - io.grpc - grpc-protobuf - - - io.grpc - grpc-stub - - - com.google.protobuf - protobuf-java + spring-boot-starter-grpc-server diff --git a/examples/03-composition/spring-boot/inventory-service/src/main/java/com/cndp/inventory/InventoryService.java b/examples/03-composition/spring-boot/inventory-service/src/main/java/com/cndp/inventory/InventoryService.java new file mode 100644 index 00000000..b68c3d81 --- /dev/null +++ b/examples/03-composition/spring-boot/inventory-service/src/main/java/com/cndp/inventory/InventoryService.java @@ -0,0 +1,53 @@ +package com.cndp.inventory; + +import java.util.Map; + +import com.cndp.proto.GetStockBatchReply; +import com.cndp.proto.GetStockBatchRequest; +import com.cndp.proto.GetStockReply; +import com.cndp.proto.GetStockRequest; +import com.cndp.proto.InventoryGrpc; +import io.grpc.stub.StreamObserver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.grpc.server.service.GrpcService; + +@GrpcService +public class InventoryService extends InventoryGrpc.InventoryImplBase { + + private static final Logger log = LoggerFactory.getLogger(InventoryService.class); + + private static final Map STOCK = Map.of( + "widget-a", 42, + "widget-b", 17, + "gadget-x", 100, + "gadget-y", 0 + ); + + @Override + public void getStock(GetStockRequest request, StreamObserver responseObserver) { + String sku = request.getSku(); + int available = STOCK.getOrDefault(sku, 0); + log.info("GetStock sku={} available={}", sku, available); + responseObserver.onNext(GetStockReply.newBuilder() + .setSku(sku) + .setAvailable(available) + .build()); + responseObserver.onCompleted(); + } + + @Override + public void getStockBatch(GetStockBatchRequest request, StreamObserver responseObserver) { + GetStockBatchReply.Builder builder = GetStockBatchReply.newBuilder(); + for (String sku : request.getSkusList()) { + int available = STOCK.getOrDefault(sku, 0); + builder.addItems(GetStockReply.newBuilder() + .setSku(sku) + .setAvailable(available) + .build()); + } + log.info("GetStockBatch skus={}", request.getSkusList()); + responseObserver.onNext(builder.build()); + responseObserver.onCompleted(); + } +} diff --git a/examples/03-composition/spring-boot/inventory-service/src/main/java/com/cndp/inventory/InventoryServiceApplication.java b/examples/03-composition/spring-boot/inventory-service/src/main/java/com/cndp/inventory/InventoryServiceApplication.java index 0e5fc61b..e54543cb 100644 --- a/examples/03-composition/spring-boot/inventory-service/src/main/java/com/cndp/inventory/InventoryServiceApplication.java +++ b/examples/03-composition/spring-boot/inventory-service/src/main/java/com/cndp/inventory/InventoryServiceApplication.java @@ -1,83 +1,13 @@ package com.cndp.inventory; -import java.util.Map; - -import com.cndp.proto.GetStockBatchReply; -import com.cndp.proto.GetStockBatchRequest; -import com.cndp.proto.GetStockReply; -import com.cndp.proto.GetStockRequest; -import com.cndp.proto.InventoryGrpc; -import io.grpc.Server; -import io.grpc.ServerBuilder; -import io.grpc.stub.StreamObserver; -import jakarta.annotation.PreDestroy; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +/** Spring gRPC starts the gRPC server (spring.grpc.server.port) and registers every @GrpcService bean. */ @SpringBootApplication -public class InventoryServiceApplication extends InventoryGrpc.InventoryImplBase implements CommandLineRunner { - - private static final Logger log = LoggerFactory.getLogger(InventoryServiceApplication.class); - - private static final Map STOCK = Map.of( - "widget-a", 42, - "widget-b", 17, - "gadget-x", 100, - "gadget-y", 0 - ); - - @Value("${grpc.port}") - private int grpcPort; - - private Server server; +public class InventoryServiceApplication { public static void main(String[] args) { SpringApplication.run(InventoryServiceApplication.class, args); } - - @Override - public void run(String... args) throws Exception { - server = ServerBuilder.forPort(grpcPort) - .addService(this) - .build() - .start(); - log.info("inventory gRPC server listening on :{}", grpcPort); - server.awaitTermination(); - } - - @PreDestroy - public void stopGrpcServer() { - if (server != null) server.shutdown(); - } - - @Override - public void getStock(GetStockRequest request, StreamObserver responseObserver) { - String sku = request.getSku(); - int available = STOCK.getOrDefault(sku, 0); - log.info("GetStock sku={} available={}", sku, available); - responseObserver.onNext(GetStockReply.newBuilder() - .setSku(sku) - .setAvailable(available) - .build()); - responseObserver.onCompleted(); - } - - @Override - public void getStockBatch(GetStockBatchRequest request, StreamObserver responseObserver) { - GetStockBatchReply.Builder builder = GetStockBatchReply.newBuilder(); - for (String sku : request.getSkusList()) { - int available = STOCK.getOrDefault(sku, 0); - builder.addItems(GetStockReply.newBuilder() - .setSku(sku) - .setAvailable(available) - .build()); - } - log.info("GetStockBatch skus={}", request.getSkusList()); - responseObserver.onNext(builder.build()); - responseObserver.onCompleted(); - } } diff --git a/examples/03-composition/spring-boot/inventory-service/src/main/resources/application.yaml b/examples/03-composition/spring-boot/inventory-service/src/main/resources/application.yaml index 10d1a556..52e85261 100644 --- a/examples/03-composition/spring-boot/inventory-service/src/main/resources/application.yaml +++ b/examples/03-composition/spring-boot/inventory-service/src/main/resources/application.yaml @@ -4,5 +4,5 @@ spring: main: web-application-type: none -grpc: - port: ${GRPC_PORT:50051} +# Spring gRPC: the server port; the standard health and reflection services are on by default. +spring.grpc.server.port: ${GRPC_PORT:50051} diff --git a/examples/11-observability/dotnet/OrderService/OrderService.csproj b/examples/11-observability/dotnet/OrderService/OrderService.csproj index da1b20b2..0d8c7c4d 100644 --- a/examples/11-observability/dotnet/OrderService/OrderService.csproj +++ b/examples/11-observability/dotnet/OrderService/OrderService.csproj @@ -11,7 +11,7 @@ - + diff --git a/examples/11-observability/dotnet/OrderService/Program.cs b/examples/11-observability/dotnet/OrderService/Program.cs index 3b4b3d9e..955586bf 100644 --- a/examples/11-observability/dotnet/OrderService/Program.cs +++ b/examples/11-observability/dotnet/OrderService/Program.cs @@ -3,7 +3,6 @@ using System.Text; using System.Text.Json; using Confluent.Kafka; -using Grpc.Net.Client; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.EntityFrameworkCore; using OpenTelemetry.Metrics; @@ -32,13 +31,12 @@ .AddOtlpExporter()); builder.Services.AddHealthChecks(); +// gRPC client factory: the channel and its HTTP/2 handler are managed like any typed HttpClient. +builder.Services.AddGrpcClient(o => o.Address = new Uri($"http://{inventoryAddr}")) + .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { EnableMultipleHttp2Connections = true }); var app = builder.Build(); -var channel = GrpcChannel.ForAddress($"http://{inventoryAddr}", - new GrpcChannelOptions { HttpHandler = new SocketsHttpHandler { EnableMultipleHttp2Connections = true } }); -var inventoryClient = new InventoryService.InventoryServiceClient(channel); - IProducer? producer = null; for (var i = 0; i < 30; i++) { @@ -60,7 +58,7 @@ ResponseWriter = (ctx, _) => ctx.Response.WriteAsJsonAsync(new { status = "ok" }), }); -app.MapPost("/orders", async (OrderRequest request, OrdersContext db) => +app.MapPost("/orders", async (OrderRequest request, OrdersContext db, InventoryService.InventoryServiceClient inventoryClient) => { var (sku, quantity) = request; var id = Guid.NewGuid().ToString(); diff --git a/examples/11-observability/dotnet/README.md b/examples/11-observability/dotnet/README.md index bd5e20f3..62562ab8 100644 --- a/examples/11-observability/dotnet/README.md +++ b/examples/11-observability/dotnet/README.md @@ -5,7 +5,7 @@ Distributed tracing, structured logging, and metrics across REST + gRPC + Kafka ## Framework & libraries - **Framework**: ASP.NET Core (.NET 10.0.12) -- **Libraries**: OpenTelemetry.Extensions.Hosting, OpenTelemetry.Instrumentation.AspNetCore, Confluent.Kafka 2.16.0, Grpc.Net.Client +- **Libraries**: OpenTelemetry.Extensions.Hosting, OpenTelemetry.Instrumentation.AspNetCore, Confluent.Kafka 2.16.0, Grpc.Net.ClientFactory (`AddGrpcClient`) - **Container base**: registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 (runtime), built with ubi9/dotnet-100:9.8-1791421718; runs as UID 1001 ## Run diff --git a/examples/11-observability/spring-boot/README.md b/examples/11-observability/spring-boot/README.md index 9ec8c5f4..c1e3955c 100644 --- a/examples/11-observability/spring-boot/README.md +++ b/examples/11-observability/spring-boot/README.md @@ -5,7 +5,7 @@ Distributed tracing, structured logging, and metrics across REST + gRPC + Kafka ## Framework & libraries - **Framework**: Spring Boot 4.1.1, Java 25 (Web, gRPC, Kafka) -- **Libraries**: spring-boot-starter-webmvc, spring-boot-starter-kafka (Spring Kafka 4), grpc-netty-shaded, OpenTelemetry API (agent supplies the SDK) +- **Libraries**: spring-boot-starter-webmvc, spring-boot-starter-kafka (Spring Kafka 4), spring-boot-starter-grpc-server (inventory) and spring-boot-starter-grpc-client (callers), Spring gRPC 1.1.1, OpenTelemetry API (agent supplies the SDK) - **Container base**: `ubi10/openjdk-25-runtime:1.24-15` (UID 185) ## Run @@ -25,7 +25,7 @@ cd .. && bash verify.sh | Path | Description | |------|-------------| | `order-service/` | REST + gRPC client + Kafka publisher with auto-traced spans | -| `inventory-service/` | gRPC server with OTel instrumentation | +| `inventory-service/` | `@GrpcService` bean served by Spring gRPC; the OTel agent instruments grpc-java underneath | | `notification-consumer/` | @KafkaListener with trace context extracted from headers | ## Implementation notes diff --git a/examples/11-observability/spring-boot/inventory-service/pom.xml b/examples/11-observability/spring-boot/inventory-service/pom.xml index fe7bca1b..c470245c 100644 --- a/examples/11-observability/spring-boot/inventory-service/pom.xml +++ b/examples/11-observability/spring-boot/inventory-service/pom.xml @@ -23,23 +23,7 @@ org.springframework.boot - spring-boot-starter - - - io.grpc - grpc-netty-shaded - - - io.grpc - grpc-protobuf - - - io.grpc - grpc-stub - - - com.google.protobuf - protobuf-java + spring-boot-starter-grpc-server io.opentelemetry diff --git a/examples/11-observability/spring-boot/inventory-service/src/main/java/com/cndp/inventory/InventoryService.java b/examples/11-observability/spring-boot/inventory-service/src/main/java/com/cndp/inventory/InventoryService.java new file mode 100644 index 00000000..0e3e319a --- /dev/null +++ b/examples/11-observability/spring-boot/inventory-service/src/main/java/com/cndp/inventory/InventoryService.java @@ -0,0 +1,64 @@ +package com.cndp.inventory; + +import java.util.concurrent.ConcurrentHashMap; + +import com.cndp.proto.InventoryServiceGrpc; +import com.cndp.proto.ReserveReply; +import com.cndp.proto.ReserveRequest; +import io.grpc.stub.StreamObserver; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.LongCounter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.grpc.server.service.GrpcService; + +@GrpcService +public class InventoryService extends InventoryServiceGrpc.InventoryServiceImplBase { + + private static final Logger log = LoggerFactory.getLogger(InventoryService.class); + + private final ConcurrentHashMap stock = new ConcurrentHashMap<>(); + private final int initialStock; + private final LongCounter stockReservationsCounter; + + public InventoryService(@Value("${inventory.initial-stock}") int initialStock) { + this.initialStock = initialStock; + // The OpenTelemetry Java agent installs the global instance and instruments the gRPC server. + this.stockReservationsCounter = GlobalOpenTelemetry.get().getMeter("inventory-service") + .counterBuilder("stock.reservations") + .setDescription("Number of stock reservation attempts") + .build(); + } + + @Override + public void reserveStock(ReserveRequest request, StreamObserver responseObserver) { + String sku = request.getSku(); + int qty = request.getQuantity(); + + stock.putIfAbsent(sku, initialStock); + + int remaining = stock.get(sku) - qty; + boolean confirmed; + if (remaining >= 0) { + stock.put(sku, remaining); + confirmed = true; + log.info("reserved sku={} qty={} remaining={}", sku, qty, remaining); + } else { + confirmed = false; + remaining = stock.get(sku); + log.warn("insufficient stock sku={} requested={} available={}", sku, qty, remaining); + } + + // Record custom metric + stockReservationsCounter.add(1, Attributes.of( + AttributeKey.stringKey("sku"), sku, + AttributeKey.booleanKey("confirmed"), confirmed)); + + responseObserver.onNext(ReserveReply.newBuilder() + .setConfirmed(confirmed).setRemaining(remaining).build()); + responseObserver.onCompleted(); + } +} diff --git a/examples/11-observability/spring-boot/inventory-service/src/main/java/com/cndp/inventory/InventoryServiceApplication.java b/examples/11-observability/spring-boot/inventory-service/src/main/java/com/cndp/inventory/InventoryServiceApplication.java index 4172a591..e54543cb 100644 --- a/examples/11-observability/spring-boot/inventory-service/src/main/java/com/cndp/inventory/InventoryServiceApplication.java +++ b/examples/11-observability/spring-boot/inventory-service/src/main/java/com/cndp/inventory/InventoryServiceApplication.java @@ -1,99 +1,13 @@ package com.cndp.inventory; -import java.util.concurrent.ConcurrentHashMap; - -import com.cndp.proto.InventoryServiceGrpc; -import com.cndp.proto.ReserveReply; -import com.cndp.proto.ReserveRequest; -import io.grpc.Server; -import io.grpc.ServerBuilder; -import io.grpc.stub.StreamObserver; -import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.metrics.LongCounter; -import io.opentelemetry.api.metrics.Meter; -import jakarta.annotation.PreDestroy; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +/** Spring gRPC starts the gRPC server (spring.grpc.server.port) and registers every @GrpcService bean. */ @SpringBootApplication -public class InventoryServiceApplication extends InventoryServiceGrpc.InventoryServiceImplBase implements CommandLineRunner { - - private static final Logger log = LoggerFactory.getLogger(InventoryServiceApplication.class); - - private final ConcurrentHashMap stock = new ConcurrentHashMap<>(); - private final OpenTelemetry openTelemetry; - - @Value("${inventory.initial-stock}") - private int initialStock; - - @Value("${grpc.port}") - private int grpcPort; - - private Server server; - private LongCounter stockReservationsCounter; - - public InventoryServiceApplication() { - // The OpenTelemetry Java agent installs the global instance and instruments the gRPC server. - this.openTelemetry = GlobalOpenTelemetry.get(); - } +public class InventoryServiceApplication { public static void main(String[] args) { SpringApplication.run(InventoryServiceApplication.class, args); } - - @Override - public void run(String... args) throws Exception { - Meter meter = openTelemetry.getMeter("inventory-service"); - stockReservationsCounter = meter.counterBuilder("stock.reservations") - .setDescription("Number of stock reservation attempts") - .build(); - - server = ServerBuilder.forPort(grpcPort) - .addService(this) - .build() - .start(); - log.info("inventory gRPC server listening on :{}", grpcPort); - server.awaitTermination(); - } - - @PreDestroy - public void stopGrpcServer() { - if (server != null) server.shutdown(); - } - - @Override - public void reserveStock(ReserveRequest request, StreamObserver responseObserver) { - String sku = request.getSku(); - int qty = request.getQuantity(); - - stock.putIfAbsent(sku, initialStock); - - int remaining = stock.get(sku) - qty; - boolean confirmed; - if (remaining >= 0) { - stock.put(sku, remaining); - confirmed = true; - log.info("reserved sku={} qty={} remaining={}", sku, qty, remaining); - } else { - confirmed = false; - remaining = stock.get(sku); - log.warn("insufficient stock sku={} requested={} available={}", sku, qty, remaining); - } - - // Record custom metric - stockReservationsCounter.add(1, Attributes.of( - AttributeKey.stringKey("sku"), sku, - AttributeKey.booleanKey("confirmed"), confirmed)); - - responseObserver.onNext(ReserveReply.newBuilder() - .setConfirmed(confirmed).setRemaining(remaining).build()); - responseObserver.onCompleted(); - } } diff --git a/examples/11-observability/spring-boot/inventory-service/src/main/resources/application.yaml b/examples/11-observability/spring-boot/inventory-service/src/main/resources/application.yaml index c726ea25..3476bd7d 100644 --- a/examples/11-observability/spring-boot/inventory-service/src/main/resources/application.yaml +++ b/examples/11-observability/spring-boot/inventory-service/src/main/resources/application.yaml @@ -4,8 +4,8 @@ spring: main: web-application-type: none -grpc: - port: ${GRPC_PORT:50051} +# Spring gRPC: the server port; the standard health and reflection services are on by default. +spring.grpc.server.port: ${GRPC_PORT:50051} inventory: initial-stock: ${INITIAL_STOCK:100} diff --git a/examples/11-observability/spring-boot/order-service/pom.xml b/examples/11-observability/spring-boot/order-service/pom.xml index 00c268c4..a0c763c9 100644 --- a/examples/11-observability/spring-boot/order-service/pom.xml +++ b/examples/11-observability/spring-boot/order-service/pom.xml @@ -42,26 +42,14 @@ postgresql runtime - - io.grpc - grpc-netty-shaded - - - io.grpc - grpc-protobuf - - - io.grpc - grpc-stub - - - com.google.protobuf - protobuf-java - io.opentelemetry opentelemetry-api + + org.springframework.boot + spring-boot-starter-grpc-client + diff --git a/examples/11-observability/spring-boot/order-service/src/main/java/com/cndp/order/OrderServiceApplication.java b/examples/11-observability/spring-boot/order-service/src/main/java/com/cndp/order/OrderServiceApplication.java index 2d62da25..bc8b0abd 100644 --- a/examples/11-observability/spring-boot/order-service/src/main/java/com/cndp/order/OrderServiceApplication.java +++ b/examples/11-observability/spring-boot/order-service/src/main/java/com/cndp/order/OrderServiceApplication.java @@ -1,26 +1,18 @@ package com.cndp.order; import com.cndp.proto.InventoryServiceGrpc; -import io.grpc.ManagedChannel; -import io.grpc.ManagedChannelBuilder; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.metrics.LongCounter; import io.opentelemetry.api.metrics.Meter; -import jakarta.annotation.PreDestroy; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; +import org.springframework.grpc.client.GrpcChannelFactory; @SpringBootApplication public class OrderServiceApplication { - @Value("${inventory.addr}") - private String inventoryAddr; - - private ManagedChannel channel; - public static void main(String[] args) { SpringApplication.run(OrderServiceApplication.class, args); } @@ -31,13 +23,10 @@ public OpenTelemetry openTelemetry() { return GlobalOpenTelemetry.get(); } + /** The "inventory" channel comes from spring.grpc.client.channel.inventory.target; Spring gRPC owns its lifecycle. */ @Bean - public InventoryServiceGrpc.InventoryServiceBlockingStub inventoryStub() { - String[] parts = inventoryAddr.split(":"); - channel = ManagedChannelBuilder.forAddress(parts[0], Integer.parseInt(parts[1])) - .usePlaintext() - .build(); - return InventoryServiceGrpc.newBlockingStub(channel); + InventoryServiceGrpc.InventoryServiceBlockingStub inventoryStub(GrpcChannelFactory channels) { + return InventoryServiceGrpc.newBlockingStub(channels.createChannel("inventory")); } @Bean @@ -47,9 +36,4 @@ public LongCounter ordersPlacedCounter(OpenTelemetry openTelemetry) { .setDescription("Number of orders placed") .build(); } - - @PreDestroy - public void shutdownChannel() { - if (channel != null) channel.shutdown(); - } } diff --git a/examples/11-observability/spring-boot/order-service/src/main/resources/application.yaml b/examples/11-observability/spring-boot/order-service/src/main/resources/application.yaml index 5329dfa2..5011f191 100644 --- a/examples/11-observability/spring-boot/order-service/src/main/resources/application.yaml +++ b/examples/11-observability/spring-boot/order-service/src/main/resources/application.yaml @@ -10,8 +10,8 @@ spring: server: port: 8080 -inventory: - addr: ${INVENTORY_ADDR:inventory:50051} +# Spring gRPC client channel (plaintext inside the compose network). +spring.grpc.client.channel.inventory.target: ${INVENTORY_ADDR:inventory:50051} logging: pattern: diff --git a/examples/18-errors/dotnet/OrderService/OrderService.csproj b/examples/18-errors/dotnet/OrderService/OrderService.csproj index 28373847..c329beaf 100644 --- a/examples/18-errors/dotnet/OrderService/OrderService.csproj +++ b/examples/18-errors/dotnet/OrderService/OrderService.csproj @@ -7,7 +7,7 @@ - + diff --git a/examples/18-errors/dotnet/OrderService/Program.cs b/examples/18-errors/dotnet/OrderService/Program.cs index dbddbfb9..a5e8d55c 100644 --- a/examples/18-errors/dotnet/OrderService/Program.cs +++ b/examples/18-errors/dotnet/OrderService/Program.cs @@ -1,7 +1,6 @@ using System.Diagnostics; using Grpc.Core; using GrpcStatusCode = Grpc.Core.StatusCode; -using Grpc.Net.Client; using Inventory.Grpc; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using OpenTelemetry.Resources; @@ -22,7 +21,8 @@ builder.Services.AddHealthChecks(); var inventoryAddr = builder.Configuration.GetValue("INVENTORY_ADDR", "http://inventory:50051"); -builder.Services.AddSingleton(new InventoryService.InventoryServiceClient(GrpcChannel.ForAddress(inventoryAddr))); +// gRPC client factory: the channel is managed like any typed HttpClient. +builder.Services.AddGrpcClient(o => o.Address = new Uri(inventoryAddr)); var app = builder.Build(); diff --git a/examples/18-errors/dotnet/README.md b/examples/18-errors/dotnet/README.md index bff14c5b..ff7699b1 100644 --- a/examples/18-errors/dotnet/README.md +++ b/examples/18-errors/dotnet/README.md @@ -5,7 +5,7 @@ RFC 9457 Problem Details for REST errors, gRPC status codes with rich error deta ## Framework & libraries - **Framework**: ASP.NET Core (.NET 10.0.12) minimal API -- **Libraries**: Grpc.Net.Client, Google.Protobuf, Grpc.Tools +- **Libraries**: Grpc.Net.ClientFactory (`AddGrpcClient`), Google.Protobuf, Grpc.Tools - **Container base**: registry.access.redhat.com/ubi9/dotnet-100-aspnet:9.8-1791363520 (runtime), built with ubi9/dotnet-100:9.8-1791421718; runs as UID 1001 ## Run @@ -35,7 +35,7 @@ Uses ASP.NET Core's built-in `Results.Problem()` which produces compliant Proble Key variables (set by compose.yaml, override for local dev): -`INVENTORY_GRPC_HOST` +`INVENTORY_ADDR` ## Local development (without containers) diff --git a/examples/18-errors/spring-boot/README.md b/examples/18-errors/spring-boot/README.md index 03af8a97..b92e560f 100644 --- a/examples/18-errors/spring-boot/README.md +++ b/examples/18-errors/spring-boot/README.md @@ -5,7 +5,7 @@ RFC 9457 Problem Details for REST errors, gRPC status codes with rich error deta ## Framework & libraries - **Framework**: Spring Boot 4.1.1, Java 25 (Web, gRPC) -- **Libraries**: spring-boot-starter-webmvc, grpc-netty-shaded, grpc-protobuf, OpenTelemetry Java agent 2.32.0 +- **Libraries**: spring-boot-starter-webmvc, spring-boot-starter-grpc-server (inventory) and spring-boot-starter-grpc-client (callers), Spring gRPC 1.1.1, OpenTelemetry Java agent 2.32.0 - **Container base**: `ubi10/openjdk-25-runtime:1.24-15` (UID 185) ## Run @@ -24,8 +24,8 @@ cd .. && bash verify.sh | Path | Description | |------|-------------| -| `order-service/` | OrderController.java — @ExceptionHandler returning ProblemDetail; GrpcErrorMapper — status code mapping | -| `inventory-service/` | InventoryGrpcService.java — gRPC server with StatusRuntimeException + Metadata | +| `order-service/` | OrderController.java and ApiErrors.java — `@ExceptionHandler` returning `ProblemDetail`, gRPC status codes mapped to 409/503; InventoryClient.java — Spring gRPC stub with a per-call deadline | +| `inventory-service/` | InventoryServiceImpl.java — `@GrpcService` bean returning gRPC status codes | ## Implementation notes @@ -37,7 +37,7 @@ The gRPC contract lives in `proto/` next to the services. The poms set the proto Key variables (set by compose.yaml, override for local dev): -`inventory.grpc.host` +`spring.grpc.client.channel.inventory.target` (from `INVENTORY_ADDR`), `inventory.deadline-ms` (from `GRPC_DEADLINE_MS`) ## Local development (without containers) diff --git a/examples/18-errors/spring-boot/inventory-service/pom.xml b/examples/18-errors/spring-boot/inventory-service/pom.xml index c1e01dfe..7c8eb229 100644 --- a/examples/18-errors/spring-boot/inventory-service/pom.xml +++ b/examples/18-errors/spring-boot/inventory-service/pom.xml @@ -23,28 +23,12 @@ org.springframework.boot - spring-boot-starter + spring-boot-starter-grpc-server - - io.grpc - grpc-netty-shaded - - - io.grpc - grpc-protobuf - - - io.grpc - grpc-stub - - - com.google.protobuf - protobuf-java - diff --git a/examples/18-errors/spring-boot/inventory-service/src/main/java/com/cndp/inventory/GrpcServerLifecycle.java b/examples/18-errors/spring-boot/inventory-service/src/main/java/com/cndp/inventory/GrpcServerLifecycle.java deleted file mode 100644 index 4f697179..00000000 --- a/examples/18-errors/spring-boot/inventory-service/src/main/java/com/cndp/inventory/GrpcServerLifecycle.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.cndp.inventory; - -import java.io.IOException; -import java.util.concurrent.TimeUnit; - -import io.grpc.Server; -import io.grpc.ServerBuilder; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.context.SmartLifecycle; -import org.springframework.stereotype.Component; - -/** Starts the gRPC server with the application context and drains it on shutdown. */ -@Component -class GrpcServerLifecycle implements SmartLifecycle { - - private static final Logger log = LoggerFactory.getLogger(GrpcServerLifecycle.class); - private static final int PORT = 50051; - - private final InventoryProperties properties; - private Server server; - - GrpcServerLifecycle(InventoryProperties properties) { - this.properties = properties; - } - - @Override - public void start() { - log.info("inventory-service starting on :{} (stock={}, fail_mode={})", - PORT, properties.initialStock(), properties.failMode()); - try { - server = ServerBuilder.forPort(PORT) - .addService(new InventoryServiceImpl(properties.initialStock(), properties.failMode())) - .build() - .start(); - } catch (IOException e) { - throw new IllegalStateException("cannot start the gRPC server on :" + PORT, e); - } - log.info("inventory-service started on :{}", PORT); - } - - @Override - public void stop() { - log.info("shutting down inventory-service"); - server.shutdown(); - try { - server.awaitTermination(5, TimeUnit.SECONDS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - - @Override - public boolean isRunning() { - return server != null && !server.isShutdown(); - } -} diff --git a/examples/18-errors/spring-boot/inventory-service/src/main/java/com/cndp/inventory/InventoryServiceImpl.java b/examples/18-errors/spring-boot/inventory-service/src/main/java/com/cndp/inventory/InventoryServiceImpl.java index 6b4df66e..51b98312 100644 --- a/examples/18-errors/spring-boot/inventory-service/src/main/java/com/cndp/inventory/InventoryServiceImpl.java +++ b/examples/18-errors/spring-boot/inventory-service/src/main/java/com/cndp/inventory/InventoryServiceImpl.java @@ -7,9 +7,12 @@ import io.grpc.stub.StreamObserver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.grpc.server.service.GrpcService; import java.util.concurrent.ConcurrentHashMap; +/** Registered with the gRPC server by Spring gRPC (spring.grpc.server.port). */ +@GrpcService public class InventoryServiceImpl extends InventoryGrpc.InventoryImplBase { private static final Logger log = LoggerFactory.getLogger(InventoryServiceImpl.class); @@ -18,9 +21,9 @@ public class InventoryServiceImpl extends InventoryGrpc.InventoryImplBase { private final String failMode; private final ConcurrentHashMap stock = new ConcurrentHashMap<>(); - public InventoryServiceImpl(int initialStock, String failMode) { - this.initialStock = initialStock; - this.failMode = failMode; + public InventoryServiceImpl(InventoryProperties properties) { + this.initialStock = properties.initialStock(); + this.failMode = properties.failMode(); } @Override diff --git a/examples/18-errors/spring-boot/inventory-service/src/main/resources/application.properties b/examples/18-errors/spring-boot/inventory-service/src/main/resources/application.properties index 0cc1c94a..54fe0e09 100644 --- a/examples/18-errors/spring-boot/inventory-service/src/main/resources/application.properties +++ b/examples/18-errors/spring-boot/inventory-service/src/main/resources/application.properties @@ -1,6 +1,6 @@ spring.main.web-application-type=none -# The gRPC server threads are daemon threads; keep the JVM alive until SIGTERM. -spring.main.keep-alive=true +# Spring gRPC: the server port; it keeps the JVM alive and drains in-flight calls on SIGTERM. +spring.grpc.server.port=${GRPC_PORT:50051} inventory.initial-stock=${INITIAL_STOCK:10} inventory.fail-mode=${FAIL_MODE:none} diff --git a/examples/18-errors/spring-boot/order-service/pom.xml b/examples/18-errors/spring-boot/order-service/pom.xml index 5cef8d54..b1bb83c7 100644 --- a/examples/18-errors/spring-boot/order-service/pom.xml +++ b/examples/18-errors/spring-boot/order-service/pom.xml @@ -35,30 +35,18 @@ - - io.grpc - grpc-netty-shaded - - - io.grpc - grpc-protobuf - - - io.grpc - grpc-stub - - - com.google.protobuf - protobuf-java - io.opentelemetry opentelemetry-api + + org.springframework.boot + spring-boot-starter-grpc-client + diff --git a/examples/18-errors/spring-boot/order-service/src/main/java/com/cndp/order/InventoryClient.java b/examples/18-errors/spring-boot/order-service/src/main/java/com/cndp/order/InventoryClient.java index 9158de1e..b10749c0 100644 --- a/examples/18-errors/spring-boot/order-service/src/main/java/com/cndp/order/InventoryClient.java +++ b/examples/18-errors/spring-boot/order-service/src/main/java/com/cndp/order/InventoryClient.java @@ -5,37 +5,21 @@ import com.cndp.proto.InventoryGrpc; import com.cndp.proto.ReserveReply; import com.cndp.proto.ReserveRequest; -import io.grpc.ManagedChannel; -import io.grpc.ManagedChannelBuilder; -import jakarta.annotation.PreDestroy; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.springframework.grpc.client.GrpcChannelFactory; import org.springframework.stereotype.Component; @Component public class InventoryClient { - private static final Logger log = LoggerFactory.getLogger(InventoryClient.class); - - private final ManagedChannel channel; private final InventoryGrpc.InventoryBlockingStub stub; private final long deadlineMs; - public InventoryClient(InventoryProperties properties) { - String[] parts = properties.addr().split(":"); - log.info("connecting to inventory at {}:{}", parts[0], parts[1]); - this.channel = ManagedChannelBuilder.forAddress(parts[0], Integer.parseInt(parts[1])) - .usePlaintext() - .build(); - this.stub = InventoryGrpc.newBlockingStub(channel); + /** The "inventory" channel comes from spring.grpc.client.channel.inventory.target; Spring gRPC owns it. */ + public InventoryClient(GrpcChannelFactory channels, InventoryProperties properties) { + this.stub = InventoryGrpc.newBlockingStub(channels.createChannel("inventory")); this.deadlineMs = properties.deadlineMs(); } - @PreDestroy - public void shutdown() { - channel.shutdown(); - } - public ReserveReply reserveStock(String sku, int quantity) { ReserveRequest request = ReserveRequest.newBuilder() .setSku(sku) diff --git a/examples/18-errors/spring-boot/order-service/src/main/java/com/cndp/order/InventoryProperties.java b/examples/18-errors/spring-boot/order-service/src/main/java/com/cndp/order/InventoryProperties.java index 918cde09..c24c902a 100644 --- a/examples/18-errors/spring-boot/order-service/src/main/java/com/cndp/order/InventoryProperties.java +++ b/examples/18-errors/spring-boot/order-service/src/main/java/com/cndp/order/InventoryProperties.java @@ -3,9 +3,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.bind.DefaultValue; -/** Where the inventory gRPC service is and how long a reservation may take. */ +/** How long a reservation may take; the channel itself is spring.grpc.client.channel.inventory. */ @ConfigurationProperties("inventory") -public record InventoryProperties( - @DefaultValue("inventory:50051") String addr, - @DefaultValue("3000") long deadlineMs) { +public record InventoryProperties(@DefaultValue("3000") long deadlineMs) { } diff --git a/examples/18-errors/spring-boot/order-service/src/main/resources/application.properties b/examples/18-errors/spring-boot/order-service/src/main/resources/application.properties index f1271d8a..f7fa87f4 100644 --- a/examples/18-errors/spring-boot/order-service/src/main/resources/application.properties +++ b/examples/18-errors/spring-boot/order-service/src/main/resources/application.properties @@ -1,7 +1,7 @@ server.port=8080 -# gRPC inventory target (bound to InventoryProperties) -inventory.addr=${INVENTORY_ADDR:inventory:50051} +# Spring gRPC client channel (plaintext inside the compose network) and the per-call deadline (InventoryProperties) +spring.grpc.client.channel.inventory.target=${INVENTORY_ADDR:inventory:50051} inventory.deadline-ms=${GRPC_DEADLINE_MS:3000} # /healthz and /readyz on the main port are the liveness and readiness probe groups. diff --git a/lgtm-presentation/Designing-Cloud-Native-APIs-Spring.pptx b/lgtm-presentation/Designing-Cloud-Native-APIs-Spring.pptx index 535e48e8..da34f3d3 100644 Binary files a/lgtm-presentation/Designing-Cloud-Native-APIs-Spring.pptx and b/lgtm-presentation/Designing-Cloud-Native-APIs-Spring.pptx differ diff --git a/lgtm-presentation/edits/Designing-Cloud-Native-APIs-Spring.fix10.json b/lgtm-presentation/edits/Designing-Cloud-Native-APIs-Spring.fix10.json new file mode 100644 index 00000000..47a6a57b --- /dev/null +++ b/lgtm-presentation/edits/Designing-Cloud-Native-APIs-Spring.fix10.json @@ -0,0 +1,110 @@ +{ + "ops": [ + { + "op": "set_code", + "sldId": 273, + "lines": [ + "// inventory.proto — the contract, registered in Apicurio", + "syntax = \"proto3\";", + "service Inventory { rpc ReserveStock (ReserveRequest) returns (ReserveReply); }", + "message ReserveRequest { string sku = 1; int32 quantity = 2; }", + "message ReserveReply { bool reserved = 1; int32 remaining = 2; }", + "// inventory-service (excerpt): Spring gRPC serves this bean on spring.grpc.server.port", + "@GrpcService", + "public class InventoryService extends InventoryGrpc.InventoryImplBase {", + " @Override public void reserveStock(ReserveRequest req, StreamObserver obs) {", + " stock.putIfAbsent(req.getSku(), initialStock);", + " int remaining = stock.get(req.getSku()) - req.getQuantity();", + " if (remaining >= 0) stock.put(req.getSku(), remaining); // reserve", + " obs.onNext(ReserveReply.newBuilder().setReserved(remaining >= 0)", + " .setRemaining(stock.get(req.getSku())).build());", + " obs.onCompleted();", + " }", + "}", + "// order-service: the channel comes from spring.grpc.client.channel.inventory.target", + "@Bean InventoryGrpc.InventoryBlockingStub inventoryStub(GrpcChannelFactory channels) {", + " return InventoryGrpc.newBlockingStub(channels.createChannel(\"inventory\"));", + "}" + ], + "_why": "examples/02 on Spring gRPC (S14)", + "_src": "s14" + }, + { + "op": "replace", + "scope": "slide", + "sldIds": [ + 273 + ], + "find": "Spring Boot 4 + grpc-java", + "repl": "Spring gRPC", + "expect": 1, + "_why": "chip", + "_src": "s14" + }, + { + "op": "replace", + "scope": "notes", + "sldIds": [ + 273 + ], + "find": "The example starts a plain grpc-java server from a CommandLineRunner inside the Spring Boot application, so no gRPC starter sits between the code and the library.", + "repl": "The inventory service is a @GrpcService bean: spring-boot-starter-grpc-server starts the server on spring.grpc.server.port, drains it on shutdown and adds the standard gRPC health service, and the order service gets its channel from GrpcChannelFactory.", + "expect": 1, + "_why": "notes follow the example", + "_src": "s14" + }, + { + "op": "set_code", + "sldId": 279, + "lines": [ + "@Controller // spring-boot-starter-graphql", + "public class OrderGraphQLController {", + " private final OrderClient orderClient;", + " private final InventoryGrpc.InventoryBlockingStub inventory; // Spring gRPC channel bean", + " public OrderGraphQLController(OrderClient o, InventoryGrpc.InventoryBlockingStub i) {", + " this.orderClient = o; this.inventory = i;", + " }", + " @QueryMapping // REST call to order-service", + " public Order order(@Argument String id) {", + " return orderClient.get(id);", + " }", + " @SchemaMapping(typeName = \"Order\", field = \"stock\") // resolved on demand", + " public Integer stock(Order order) { // via gRPC", + " return inventory.getStock(GetStockRequest.newBuilder().setSku(order.sku()).build())", + " .getAvailable();", + " }", + " // @BatchMapping(\"stock\") batches N orders into one gRPC call: no N+1", + "}" + ], + "_why": "no InventoryGrpcClient type exists; the example injects the generated stub", + "_src": "s14" + }, + { + "op": "set_code", + "sldId": 319, + "lines": [ + "# Containerfile — attach the OpenTelemetry Java agent (Spring Boot 4 needs no starter)", + "ENV JAVA_TOOL_OPTIONS=\"-javaagent:/deployments/opentelemetry-javaagent.jar\"", + "# environment: OTEL_SERVICE_NAME=order-service", + "# OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318", + "# OTEL_TRACES_SAMPLER=parentbased_traceidratio, OTEL_TRACES_SAMPLER_ARG=1.0", + "// The agent instruments Spring MVC, RestClient, gRPC, JDBC and the Kafka clients,", + "// and puts trace_id/span_id in the logback MDC. Custom business spans use the", + "// OpenTelemetry API annotation (io.opentelemetry.instrumentation.annotations):", + "@Service", + "public class OrderService {", + " private final InventoryServiceGrpc.InventoryServiceBlockingStub inventory; // Spring gRPC", + " public OrderService(InventoryServiceGrpc.InventoryServiceBlockingStub i) { inventory = i; }", + " @WithSpan(\"reserve_stock\") // custom span", + " public ReserveReply reserve(String sku, int qty) {", + " return inventory.reserveStock(", + " ReserveRequest.newBuilder().setSku(sku).setQuantity(qty).build());", + " }", + " // trace_id flows: REST -> gRPC -> Kafka -> consumer", + "}" + ], + "_why": "no InventoryGrpcClient type exists; the example injects the generated stub", + "_src": "s14" + } + ] +} \ No newline at end of file diff --git a/lgtm-presentation/text/Designing-Cloud-Native-APIs-Spring.md b/lgtm-presentation/text/Designing-Cloud-Native-APIs-Spring.md index b7a7d3ae..e3ce92db 100644 --- a/lgtm-presentation/text/Designing-Cloud-Native-APIs-Spring.md +++ b/lgtm-presentation/text/Designing-Cloud-Native-APIs-Spring.md @@ -246,7 +246,7 @@ p. 14 ## [018] COMMUNICATIONS · gRPC for internal service-to-service calls (sldId 273) -[chip] protobuf + java · Spring Boot 4 + grpc-java +[chip] protobuf + java · Spring gRPC ```protobuf+java // inventory.proto — the contract, registered in Apicurio @@ -254,22 +254,22 @@ syntax = "proto3"; service Inventory { rpc ReserveStock (ReserveRequest) returns (ReserveReply); } message ReserveRequest { string sku = 1; int32 quantity = 2; } message ReserveReply { bool reserved = 1; int32 remaining = 2; } -// inventory-service (excerpt): grpc-java server; protobuf-maven-plugin generates the stubs -@SpringBootApplication -public class InventoryServiceApplication extends InventoryGrpc.InventoryImplBase - implements CommandLineRunner { - @Override public void run(String... args) throws Exception { - server = ServerBuilder.forPort(grpcPort).addService(this).build().start(); - server.awaitTermination(); - } +// inventory-service (excerpt): Spring gRPC serves this bean on spring.grpc.server.port +@GrpcService +public class InventoryService extends InventoryGrpc.InventoryImplBase { @Override public void reserveStock(ReserveRequest req, StreamObserver obs) { + stock.putIfAbsent(req.getSku(), initialStock); int remaining = stock.get(req.getSku()) - req.getQuantity(); if (remaining >= 0) stock.put(req.getSku(), remaining); // reserve obs.onNext(ReserveReply.newBuilder().setReserved(remaining >= 0) - .setRemaining(remaining >= 0 ? remaining : stock.get(req.getSku())).build()); + .setRemaining(stock.get(req.getSku())).build()); obs.onCompleted(); } } +// order-service: the channel comes from spring.grpc.client.channel.inventory.target +@Bean InventoryGrpc.InventoryBlockingStub inventoryStub(GrpcChannelFactory channels) { + return InventoryGrpc.newBlockingStub(channels.createChannel("inventory")); +} ``` Schema-first, binary, multiplexed over HTTP/2 — the right default for chatty internal paths. Test with ghz. @@ -278,7 +278,7 @@ p. 15 > gRPC for the internal paths. The .proto is the contract, and it lives in the registry like OpenAPI. The protobuf-maven-plugin runs protoc at build time; the service extends the generated InventoryImplBase and fills in the one method, never touching the wire format. > -> The example starts a plain grpc-java server from a CommandLineRunner inside the Spring Boot application, so no gRPC starter sits between the code and the library. The field numbers (=1, =2) are the wire contract; Appendix B explains why they can never be reused. +> The inventory service is a @GrpcService bean: spring-boot-starter-grpc-server starts the server on spring.grpc.server.port, drains it on shutdown and adds the standard gRPC health service, and the order service gets its channel from GrpcChannelFactory. The field numbers (=1, =2) are the wire contract; Appendix B explains why they can never be reused. > > gRPC is binary and multiplexed over HTTP/2, so it is fast and supports streaming: the right default for chatty service-to-service calls where you control both ends. Load-test it with ghz. @@ -349,8 +349,8 @@ p. 19 @Controller // spring-boot-starter-graphql public class OrderGraphQLController { private final OrderClient orderClient; - private final InventoryGrpcClient inventory; - public OrderGraphQLController(OrderClient o, InventoryGrpcClient i) { + private final InventoryGrpc.InventoryBlockingStub inventory; // Spring gRPC channel bean + public OrderGraphQLController(OrderClient o, InventoryGrpc.InventoryBlockingStub i) { this.orderClient = o; this.inventory = i; } @QueryMapping // REST call to order-service @@ -359,7 +359,8 @@ public class OrderGraphQLController { } @SchemaMapping(typeName = "Order", field = "stock") // resolved on demand public Integer stock(Order order) { // via gRPC - return inventory.remaining(order.sku()); + return inventory.getStock(GetStockRequest.newBuilder().setSku(order.sku()).build()) + .getAvailable(); } // @BatchMapping("stock") batches N orders into one gRPC call: no N+1 } @@ -882,11 +883,12 @@ ENV JAVA_TOOL_OPTIONS="-javaagent:/deployments/opentelemetry-javaagent.jar" // OpenTelemetry API annotation (io.opentelemetry.instrumentation.annotations): @Service public class OrderService { - private final InventoryGrpcClient inventory; - public OrderService(InventoryGrpcClient i) { this.inventory = i; } + private final InventoryServiceGrpc.InventoryServiceBlockingStub inventory; // Spring gRPC + public OrderService(InventoryServiceGrpc.InventoryServiceBlockingStub i) { inventory = i; } @WithSpan("reserve_stock") // custom span public ReserveReply reserve(String sku, int qty) { - return inventory.reserveStock(sku, qty); + return inventory.reserveStock( + ReserveRequest.newBuilder().setSku(sku).setQuantity(qty).build()); } // trace_id flows: REST -> gRPC -> Kafka -> consumer }