Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions docs/asciidoc/gRPC.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
== gRPC

The `jooby-grpc` module provides first-class, native support for https://grpc.io/[gRPC].

Unlike traditional setups that require spinning up a separate gRPC server on a different port (often forcing a specific transport like Netty), this module embeds the `grpc-java` engine directly into Jooby.

By using a custom native bridge, it allows you to run strictly-typed gRPC services alongside your standard REST API routes on the **exact same port**. It bypasses the standard HTTP/1.1 pipeline in favor of a highly optimized, native interceptor tailored for HTTP/2 multiplexing, reactive backpressure, and zero-copy byte framing. It works natively across Undertow, Netty, and Jetty.

=== Dependency

[source, xml, role="primary"]
.Maven
----
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-grpc</artifactId>
<version>${jooby.version}</version>
</dependency>
----

[source, gradle, role="secondary"]
.Gradle
----
implementation 'io.jooby:jooby-grpc:${jooby.version}'
----

=== Usage

gRPC strictly requires HTTP/2. Before installing the module, ensure your application is configured to use a supported server with HTTP/2 enabled.

[source, java]
----
import io.jooby.Jooby;
import io.jooby.ServerOptions;
import io.jooby.grpc.GrpcModule;

public class App extends Jooby {
{
setServerOptions(new ServerOptions().setHttp2(true)); // <1>

install(new GrpcModule( // <2>
new GreeterService()
));

get("/api/health", ctx -> "OK"); // <3>
}

public static void main(String[] args) {
runApp(args, App::new);
}
}
----
<1> Enable HTTP/2 on your server.
<2> Install the module and explicitly register your services.
<3> Standard REST routes still work on the exact same port!

=== Dependency Injection

If your gRPC services require external dependencies (like database repositories), you can register the service classes instead of pre-instantiated objects. The module will automatically provision them using your active Dependency Injection framework (e.g., Guice, Spring).

[source, java]
----
import io.jooby.Jooby;
import io.jooby.di.GuiceModule;
import io.jooby.grpc.GrpcModule;

public class App extends Jooby {
{
install(new GuiceModule());

install(new GrpcModule(
GreeterService.class // <1>
));
}
}
----
<1> Pass the class references. The DI framework will instantiate them.

WARNING: gRPC services are registered as **Singletons**. Ensure your service implementations are thread-safe and do not hold request-scoped state in instance variables. Heavy blocking operations will safely run on background workers, protecting the native server's I/O event loops.

=== Server Reflection

If you want to use tools like `grpcurl` or Postman to interact with your services without providing the `.proto` files, you can easily enable gRPC Server Reflection.

Include the `grpc-services` dependency in your build, and register the v1 reflection service alongside your own:

[source, java]
----
import io.grpc.protobuf.services.ProtoReflectionServiceV1;

public class App extends Jooby {
{
install(new GrpcModule(
new GreeterService(),
ProtoReflectionServiceV1.newInstance() // <1>
));
}
}
----
<1> Enables the modern `v1` reflection protocol for maximum compatibility with gRPC clients.

=== Routing & Fallbacks

The gRPC module intercepts requests natively before they reach Jooby's standard router.

If a client attempts to call a gRPC method that does not exist, the request gracefully falls through to the standard Jooby router, returning a native `404 Not Found` (which gRPC clients will automatically translate to a Status `12 UNIMPLEMENTED`).

If you misconfigure your server (e.g., attempting to run gRPC over HTTP/1.1), the fallback route will catch the request and throw an `IllegalStateException` to help you identify the missing configuration immediately.
83 changes: 83 additions & 0 deletions jooby/src/main/java/io/jooby/GrpcExchange.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Jooby https://jooby.io
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
* Copyright 2014 Edgar Espina
*/
package io.jooby;

import java.nio.ByteBuffer;
import java.util.Map;
import java.util.function.Consumer;

import edu.umd.cs.findbugs.annotations.Nullable;

/**
* Server-agnostic abstraction for a native HTTP/2 gRPC exchange.
*
* <p>This interface bridges the gap between the underlying web server (Undertow, Netty, or Jetty)
* and the reactive gRPC processor. Because gRPC heavily relies on HTTP/2 multiplexing, asynchronous
* I/O, and trailing headers (trailers) to communicate status codes, standard HTTP/1.1 context
* abstractions are insufficient.
*
* <p>Native server interceptors wrap their respective request/response objects into this interface,
* allowing the {@link GrpcProcessor} to read headers, push zero-copy byte frames, and finalize the
* stream with standard gRPC trailers without knowing which server engine is actually running.
*/
public interface GrpcExchange {

/**
* Retrieves the requested URI path.
*
* <p>In gRPC, this dictates the routing and strictly follows the pattern: {@code
* /Fully.Qualified.ServiceName/MethodName}.
*
* @return The exact path of the incoming HTTP/2 request.
*/
String getRequestPath();

/**
* Retrieves the value of the specified HTTP request header.
*
* @param name The name of the header (case-insensitive).
* @return The header value, or {@code null} if the header is not present.
*/
@Nullable String getHeader(String name);

/**
* Retrieves all HTTP request headers.
*
* @return A map containing all headers provided by the client.
*/
Map<String, String> getHeaders();

/**
* Writes a gRPC-framed byte payload to the underlying non-blocking socket.
*
* <p>This method must push the buffer to the native network layer without blocking the invoking
* thread (which is typically a background gRPC worker). The implementation is responsible for
* translating the ByteBuffer into the server's native data format and flushing it over the
* network.
*
* @param payload The properly framed 5-byte-prefixed gRPC payload to send.
* @param onFailure A callback invoked immediately if the asynchronous network write fails (e.g.,
* if the client abruptly disconnects or the channel is closed).
*/
void send(ByteBuffer payload, Consumer<Throwable> onFailure);

/**
* Closes the HTTP/2 stream by appending the mandatory gRPC trailing headers.
*
* <p>In the gRPC specification, a successful response or an application-level error is
* communicated not by standard HTTP status codes (which are always 200 OK), but by appending
* HTTP/2 trailers ({@code grpc-status} and {@code grpc-message}) at the very end of the stream.
*
* <p>Calling this method informs the native server to write those trailing headers and formally
* close the bidirectional stream.
*
* @param statusCode The gRPC integer status code (e.g., {@code 0} for OK, {@code 12} for
* UNIMPLEMENTED, {@code 4} for DEADLINE_EXCEEDED).
* @param description An optional, human-readable status message detailing the result or error.
* May be {@code null}.
*/
void close(int statusCode, @Nullable String description);
}
51 changes: 51 additions & 0 deletions jooby/src/main/java/io/jooby/GrpcProcessor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Jooby https://jooby.io
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
* Copyright 2014 Edgar Espina
*/
package io.jooby;

import java.nio.ByteBuffer;
import java.util.concurrent.Flow;

import edu.umd.cs.findbugs.annotations.NonNull;

/**
* Core Service Provider Interface (SPI) for the gRPC extension.
*
* <p>This processor acts as the bridge between the native HTTP/2 web servers and the embedded gRPC
* engine. It is designed to intercept and process gRPC exchanges at the lowest possible network
* level, completely bypassing Jooby's standard HTTP/1.1 routing pipeline. This architecture ensures
* strict HTTP/2 specification compliance, zero-copy buffering, and reactive backpressure.
*/
public interface GrpcProcessor {

/**
* Checks if the given URI path exactly matches a registered gRPC method.
*
* <p>Native server interceptors use this method as a lightweight, fail-fast guard. If this
* returns {@code true}, the server will hijack the request and upgrade it to a native gRPC
* stream. If {@code false}, the request safely falls through to the standard Jooby router
* (typically resulting in a standard HTTP 404 Not Found, which gRPC clients gracefully translate
* to Status 12 UNIMPLEMENTED).
*
* @param path The incoming request path (e.g., {@code /fully.qualified.Service/MethodName}).
* @return {@code true} if the path is mapped to an active gRPC service; {@code false} otherwise.
*/
boolean isGrpcMethod(String path);

/**
* Initiates the reactive gRPC pipeline for an incoming HTTP/2 request.
*
* <p>When a valid gRPC request is intercepted, the native server wraps the underlying network
* connection into a {@link GrpcExchange} and passes it to this method. The processor uses this
* exchange to asynchronously send response headers, payload byte frames, and HTTP/2 trailers.
*
* @param exchange The native server exchange representing the bidirectional HTTP/2 stream.
* @return A reactive {@link Flow.Subscriber} that the native server must feed incoming request
* payload {@link ByteBuffer} chunks into. Returns {@code null} if the exchange was rejected.
* @throws IllegalStateException If an unregistered path bypasses the {@link
* #isGrpcMethod(String)} guard.
*/
Flow.Subscriber<ByteBuffer> process(@NonNull GrpcExchange exchange);
}
2 changes: 1 addition & 1 deletion jooby/src/main/java/io/jooby/ServerOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ public int getPort() {
* @return True when SSL is enabled. Either bc the secure port, httpsOnly or SSL options are set.
*/
public boolean isSSLEnabled() {
return securePort != null || ssl != null || httpsOnly;
return securePort != null || ssl != null || http2 == Boolean.TRUE || httpsOnly;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
*/
package io.jooby.internal.apt;

import javax.lang.model.element.*;
import static io.jooby.internal.apt.AnnotationSupport.findAnnotationValue;
import static io.jooby.internal.apt.Types.BUILT_IN;
import static java.util.stream.Collectors.joining;

import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Stream;

import static io.jooby.internal.apt.AnnotationSupport.findAnnotationValue;
import static io.jooby.internal.apt.Types.BUILT_IN;
import static java.util.stream.Collectors.joining;
import javax.lang.model.element.*;

public enum ParameterGenerator {
ContextParam("getAttribute", "io.jooby.annotation.ContextParam", "jakarta.ws.rs.core.Context") {
Expand Down Expand Up @@ -440,11 +441,13 @@ protected String defaultValue(VariableElement parameter, AnnotationMirror annota
var sources = findAnnotationValue(annotation, AnnotationSupport.VALUE);
return sources.isEmpty() ? "" : CodeBlock.of(", ", CodeBlock.string(sources.getFirst()));
} else if (annotationType.startsWith("jakarta.ws.rs")) {
var defaultValueAnnotation = AnnotationSupport.findAnnotationByName(
parameter, "jakarta.ws.rs.DefaultValue");
var defaultValueAnnotation =
AnnotationSupport.findAnnotationByName(parameter, "jakarta.ws.rs.DefaultValue");
if (defaultValueAnnotation != null) {
var defaultValue = findAnnotationValue(defaultValueAnnotation, AnnotationSupport.VALUE);
return defaultValue.isEmpty() ? "" : CodeBlock.of(", ", CodeBlock.string(defaultValue.getFirst()));
return defaultValue.isEmpty()
? ""
: CodeBlock.of(", ", CodeBlock.string(defaultValue.getFirst()));
}
}
return "";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
import io.jooby.annotation.GET;
import io.jooby.annotation.Path;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.FormParam;
import jakarta.ws.rs.QueryParam;

@Path("/3761")
public class C3761Jakarta {
Expand Down
14 changes: 6 additions & 8 deletions modules/jooby-apt/src/test/java/tests/i3761/Issue3761.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
*/
package tests.i3761;

import io.jooby.apt.ProcessorRunner;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertTrue;
import io.jooby.apt.ProcessorRunner;

public class Issue3761 {
@Test
Expand All @@ -26,11 +27,8 @@ public void shouldGenerateJakartaDefaultValues() throws Exception {
private static void assertSourceCodeRespectDefaultValues(String source) {
assertTrue(source.contains("return c.number(ctx.query(\"num\", \"5\").intValue());"));
assertTrue(source.contains("return c.unset(ctx.query(\"unset\").valueOrNull());"));
assertTrue(
source.contains("return c.emptySet(ctx.query(\"emptySet\", \"\").value());"));
assertTrue(
source.contains("return c.string(ctx.query(\"stringVal\", \"Hello\").value());"));
assertTrue(
source.contains("return c.bool(ctx.form(\"boolVal\", \"false\").booleanValue());"));
assertTrue(source.contains("return c.emptySet(ctx.query(\"emptySet\", \"\").value());"));
assertTrue(source.contains("return c.string(ctx.query(\"stringVal\", \"Hello\").value());"));
assertTrue(source.contains("return c.bool(ctx.form(\"boolVal\", \"false\").booleanValue());"));
}
}
5 changes: 5 additions & 0 deletions modules/jooby-bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@
<artifactId>jooby-graphql</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-grpc</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-gson</artifactId>
Expand Down
Loading
Loading