Skip to content

Commit d02c15a

Browse files
authored
Merge pull request #3876 from jooby-project/grpc
gRPC Support
2 parents ea8ebcb + ef7540f commit d02c15a

47 files changed

Lines changed: 2965 additions & 46 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/asciidoc/gRPC.adoc

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
== gRPC
2+
3+
The `jooby-grpc` module provides first-class, native support for https://grpc.io/[gRPC].
4+
5+
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.
6+
7+
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.
8+
9+
=== Dependency
10+
11+
[source, xml, role="primary"]
12+
.Maven
13+
----
14+
<dependency>
15+
<groupId>io.jooby</groupId>
16+
<artifactId>jooby-grpc</artifactId>
17+
<version>${jooby.version}</version>
18+
</dependency>
19+
----
20+
21+
[source, gradle, role="secondary"]
22+
.Gradle
23+
----
24+
implementation 'io.jooby:jooby-grpc:${jooby.version}'
25+
----
26+
27+
=== Usage
28+
29+
gRPC strictly requires HTTP/2. Before installing the module, ensure your application is configured to use a supported server with HTTP/2 enabled.
30+
31+
[source, java]
32+
----
33+
import io.jooby.Jooby;
34+
import io.jooby.ServerOptions;
35+
import io.jooby.grpc.GrpcModule;
36+
37+
public class App extends Jooby {
38+
{
39+
setServerOptions(new ServerOptions().setHttp2(true)); // <1>
40+
41+
install(new GrpcModule( // <2>
42+
new GreeterService()
43+
));
44+
45+
get("/api/health", ctx -> "OK"); // <3>
46+
}
47+
48+
public static void main(String[] args) {
49+
runApp(args, App::new);
50+
}
51+
}
52+
----
53+
<1> Enable HTTP/2 on your server.
54+
<2> Install the module and explicitly register your services.
55+
<3> Standard REST routes still work on the exact same port!
56+
57+
=== Dependency Injection
58+
59+
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).
60+
61+
[source, java]
62+
----
63+
import io.jooby.Jooby;
64+
import io.jooby.di.GuiceModule;
65+
import io.jooby.grpc.GrpcModule;
66+
67+
public class App extends Jooby {
68+
{
69+
install(new GuiceModule());
70+
71+
install(new GrpcModule(
72+
GreeterService.class // <1>
73+
));
74+
}
75+
}
76+
----
77+
<1> Pass the class references. The DI framework will instantiate them.
78+
79+
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.
80+
81+
=== Server Reflection
82+
83+
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.
84+
85+
Include the `grpc-services` dependency in your build, and register the v1 reflection service alongside your own:
86+
87+
[source, java]
88+
----
89+
import io.grpc.protobuf.services.ProtoReflectionServiceV1;
90+
91+
public class App extends Jooby {
92+
{
93+
install(new GrpcModule(
94+
new GreeterService(),
95+
ProtoReflectionServiceV1.newInstance() // <1>
96+
));
97+
}
98+
}
99+
----
100+
<1> Enables the modern `v1` reflection protocol for maximum compatibility with gRPC clients.
101+
102+
=== Routing & Fallbacks
103+
104+
The gRPC module intercepts requests natively before they reach Jooby's standard router.
105+
106+
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`).
107+
108+
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.
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby;
7+
8+
import java.nio.ByteBuffer;
9+
import java.util.Map;
10+
import java.util.function.Consumer;
11+
12+
import edu.umd.cs.findbugs.annotations.Nullable;
13+
14+
/**
15+
* Server-agnostic abstraction for a native HTTP/2 gRPC exchange.
16+
*
17+
* <p>This interface bridges the gap between the underlying web server (Undertow, Netty, or Jetty)
18+
* and the reactive gRPC processor. Because gRPC heavily relies on HTTP/2 multiplexing, asynchronous
19+
* I/O, and trailing headers (trailers) to communicate status codes, standard HTTP/1.1 context
20+
* abstractions are insufficient.
21+
*
22+
* <p>Native server interceptors wrap their respective request/response objects into this interface,
23+
* allowing the {@link GrpcProcessor} to read headers, push zero-copy byte frames, and finalize the
24+
* stream with standard gRPC trailers without knowing which server engine is actually running.
25+
*/
26+
public interface GrpcExchange {
27+
28+
/**
29+
* Retrieves the requested URI path.
30+
*
31+
* <p>In gRPC, this dictates the routing and strictly follows the pattern: {@code
32+
* /Fully.Qualified.ServiceName/MethodName}.
33+
*
34+
* @return The exact path of the incoming HTTP/2 request.
35+
*/
36+
String getRequestPath();
37+
38+
/**
39+
* Retrieves the value of the specified HTTP request header.
40+
*
41+
* @param name The name of the header (case-insensitive).
42+
* @return The header value, or {@code null} if the header is not present.
43+
*/
44+
@Nullable String getHeader(String name);
45+
46+
/**
47+
* Retrieves all HTTP request headers.
48+
*
49+
* @return A map containing all headers provided by the client.
50+
*/
51+
Map<String, String> getHeaders();
52+
53+
/**
54+
* Writes a gRPC-framed byte payload to the underlying non-blocking socket.
55+
*
56+
* <p>This method must push the buffer to the native network layer without blocking the invoking
57+
* thread (which is typically a background gRPC worker). The implementation is responsible for
58+
* translating the ByteBuffer into the server's native data format and flushing it over the
59+
* network.
60+
*
61+
* @param payload The properly framed 5-byte-prefixed gRPC payload to send.
62+
* @param onFailure A callback invoked immediately if the asynchronous network write fails (e.g.,
63+
* if the client abruptly disconnects or the channel is closed).
64+
*/
65+
void send(ByteBuffer payload, Consumer<Throwable> onFailure);
66+
67+
/**
68+
* Closes the HTTP/2 stream by appending the mandatory gRPC trailing headers.
69+
*
70+
* <p>In the gRPC specification, a successful response or an application-level error is
71+
* communicated not by standard HTTP status codes (which are always 200 OK), but by appending
72+
* HTTP/2 trailers ({@code grpc-status} and {@code grpc-message}) at the very end of the stream.
73+
*
74+
* <p>Calling this method informs the native server to write those trailing headers and formally
75+
* close the bidirectional stream.
76+
*
77+
* @param statusCode The gRPC integer status code (e.g., {@code 0} for OK, {@code 12} for
78+
* UNIMPLEMENTED, {@code 4} for DEADLINE_EXCEEDED).
79+
* @param description An optional, human-readable status message detailing the result or error.
80+
* May be {@code null}.
81+
*/
82+
void close(int statusCode, @Nullable String description);
83+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby;
7+
8+
import java.nio.ByteBuffer;
9+
import java.util.concurrent.Flow;
10+
11+
import edu.umd.cs.findbugs.annotations.NonNull;
12+
13+
/**
14+
* Core Service Provider Interface (SPI) for the gRPC extension.
15+
*
16+
* <p>This processor acts as the bridge between the native HTTP/2 web servers and the embedded gRPC
17+
* engine. It is designed to intercept and process gRPC exchanges at the lowest possible network
18+
* level, completely bypassing Jooby's standard HTTP/1.1 routing pipeline. This architecture ensures
19+
* strict HTTP/2 specification compliance, zero-copy buffering, and reactive backpressure.
20+
*/
21+
public interface GrpcProcessor {
22+
23+
/**
24+
* Checks if the given URI path exactly matches a registered gRPC method.
25+
*
26+
* <p>Native server interceptors use this method as a lightweight, fail-fast guard. If this
27+
* returns {@code true}, the server will hijack the request and upgrade it to a native gRPC
28+
* stream. If {@code false}, the request safely falls through to the standard Jooby router
29+
* (typically resulting in a standard HTTP 404 Not Found, which gRPC clients gracefully translate
30+
* to Status 12 UNIMPLEMENTED).
31+
*
32+
* @param path The incoming request path (e.g., {@code /fully.qualified.Service/MethodName}).
33+
* @return {@code true} if the path is mapped to an active gRPC service; {@code false} otherwise.
34+
*/
35+
boolean isGrpcMethod(String path);
36+
37+
/**
38+
* Initiates the reactive gRPC pipeline for an incoming HTTP/2 request.
39+
*
40+
* <p>When a valid gRPC request is intercepted, the native server wraps the underlying network
41+
* connection into a {@link GrpcExchange} and passes it to this method. The processor uses this
42+
* exchange to asynchronously send response headers, payload byte frames, and HTTP/2 trailers.
43+
*
44+
* @param exchange The native server exchange representing the bidirectional HTTP/2 stream.
45+
* @return A reactive {@link Flow.Subscriber} that the native server must feed incoming request
46+
* payload {@link ByteBuffer} chunks into. Returns {@code null} if the exchange was rejected.
47+
* @throws IllegalStateException If an unregistered path bypasses the {@link
48+
* #isGrpcMethod(String)} guard.
49+
*/
50+
Flow.Subscriber<ByteBuffer> process(@NonNull GrpcExchange exchange);
51+
}

jooby/src/main/java/io/jooby/ServerOptions.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ public int getPort() {
290290
* @return True when SSL is enabled. Either bc the secure port, httpsOnly or SSL options are set.
291291
*/
292292
public boolean isSSLEnabled() {
293-
return securePort != null || ssl != null || httpsOnly;
293+
return securePort != null || ssl != null || http2 == Boolean.TRUE || httpsOnly;
294294
}
295295

296296
/**

modules/jooby-apt/src/main/java/io/jooby/internal/apt/ParameterGenerator.java

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@
55
*/
66
package io.jooby.internal.apt;
77

8-
import javax.lang.model.element.*;
8+
import static io.jooby.internal.apt.AnnotationSupport.findAnnotationValue;
9+
import static io.jooby.internal.apt.Types.BUILT_IN;
10+
import static java.util.stream.Collectors.joining;
11+
912
import java.util.*;
1013
import java.util.function.Predicate;
1114
import java.util.stream.Stream;
1215

13-
import static io.jooby.internal.apt.AnnotationSupport.findAnnotationValue;
14-
import static io.jooby.internal.apt.Types.BUILT_IN;
15-
import static java.util.stream.Collectors.joining;
16+
import javax.lang.model.element.*;
1617

1718
public enum ParameterGenerator {
1819
ContextParam("getAttribute", "io.jooby.annotation.ContextParam", "jakarta.ws.rs.core.Context") {
@@ -440,11 +441,13 @@ protected String defaultValue(VariableElement parameter, AnnotationMirror annota
440441
var sources = findAnnotationValue(annotation, AnnotationSupport.VALUE);
441442
return sources.isEmpty() ? "" : CodeBlock.of(", ", CodeBlock.string(sources.getFirst()));
442443
} else if (annotationType.startsWith("jakarta.ws.rs")) {
443-
var defaultValueAnnotation = AnnotationSupport.findAnnotationByName(
444-
parameter, "jakarta.ws.rs.DefaultValue");
444+
var defaultValueAnnotation =
445+
AnnotationSupport.findAnnotationByName(parameter, "jakarta.ws.rs.DefaultValue");
445446
if (defaultValueAnnotation != null) {
446447
var defaultValue = findAnnotationValue(defaultValueAnnotation, AnnotationSupport.VALUE);
447-
return defaultValue.isEmpty() ? "" : CodeBlock.of(", ", CodeBlock.string(defaultValue.getFirst()));
448+
return defaultValue.isEmpty()
449+
? ""
450+
: CodeBlock.of(", ", CodeBlock.string(defaultValue.getFirst()));
448451
}
449452
}
450453
return "";

modules/jooby-apt/src/test/java/tests/i3761/C3761Jakarta.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
import io.jooby.annotation.GET;
99
import io.jooby.annotation.Path;
1010
import jakarta.ws.rs.DefaultValue;
11-
import jakarta.ws.rs.QueryParam;
1211
import jakarta.ws.rs.FormParam;
12+
import jakarta.ws.rs.QueryParam;
1313

1414
@Path("/3761")
1515
public class C3761Jakarta {

modules/jooby-apt/src/test/java/tests/i3761/Issue3761.java

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
*/
66
package tests.i3761;
77

8-
import io.jooby.apt.ProcessorRunner;
8+
import static org.junit.jupiter.api.Assertions.assertTrue;
9+
910
import org.junit.jupiter.api.Test;
1011

11-
import static org.junit.jupiter.api.Assertions.assertTrue;
12+
import io.jooby.apt.ProcessorRunner;
1213

1314
public class Issue3761 {
1415
@Test
@@ -26,11 +27,8 @@ public void shouldGenerateJakartaDefaultValues() throws Exception {
2627
private static void assertSourceCodeRespectDefaultValues(String source) {
2728
assertTrue(source.contains("return c.number(ctx.query(\"num\", \"5\").intValue());"));
2829
assertTrue(source.contains("return c.unset(ctx.query(\"unset\").valueOrNull());"));
29-
assertTrue(
30-
source.contains("return c.emptySet(ctx.query(\"emptySet\", \"\").value());"));
31-
assertTrue(
32-
source.contains("return c.string(ctx.query(\"stringVal\", \"Hello\").value());"));
33-
assertTrue(
34-
source.contains("return c.bool(ctx.form(\"boolVal\", \"false\").booleanValue());"));
30+
assertTrue(source.contains("return c.emptySet(ctx.query(\"emptySet\", \"\").value());"));
31+
assertTrue(source.contains("return c.string(ctx.query(\"stringVal\", \"Hello\").value());"));
32+
assertTrue(source.contains("return c.bool(ctx.form(\"boolVal\", \"false\").booleanValue());"));
3533
}
3634
}

modules/jooby-bom/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,11 @@
115115
<artifactId>jooby-graphql</artifactId>
116116
<version>${project.version}</version>
117117
</dependency>
118+
<dependency>
119+
<groupId>io.jooby</groupId>
120+
<artifactId>jooby-grpc</artifactId>
121+
<version>${project.version}</version>
122+
</dependency>
118123
<dependency>
119124
<groupId>io.jooby</groupId>
120125
<artifactId>jooby-gson</artifactId>

0 commit comments

Comments
 (0)