Skip to content

Commit 8c24d29

Browse files
committed
- add DI support to gRPC services
1 parent ca3695b commit 8c24d29

6 files changed

Lines changed: 104 additions & 54 deletions

File tree

modules/jooby-grpc/src/main/java/io/jooby/grpc/GrpcModule.java

Lines changed: 62 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,21 @@
55
*/
66
package io.jooby.grpc;
77

8-
import java.util.HashMap;
9-
import java.util.List;
10-
import java.util.Map;
8+
import java.util.*;
119

1210
import org.slf4j.bridge.SLF4JBridgeHandler;
1311

1412
import edu.umd.cs.findbugs.annotations.NonNull;
1513
import io.grpc.BindableService;
1614
import io.grpc.MethodDescriptor;
17-
import io.grpc.Server;
1815
import io.grpc.inprocess.InProcessChannelBuilder;
1916
import io.grpc.inprocess.InProcessServerBuilder;
2017
import io.jooby.*;
2118
import io.jooby.internal.grpc.DefaultGrpcProcessor;
2219

2320
public class GrpcModule implements Extension {
24-
private final List<BindableService> services;
25-
private final Map<String, MethodDescriptor<?, ?>> registry = new HashMap<>();
26-
private Server grpcServer;
21+
private final List<BindableService> services = new ArrayList<>();
22+
private final List<Class<? extends BindableService>> serviceClasses = new ArrayList<>();
2723

2824
static {
2925
// Optionally remove existing handlers attached to the j.u.l root logger
@@ -33,52 +29,78 @@ public class GrpcModule implements Extension {
3329
}
3430

3531
public GrpcModule(BindableService... services) {
36-
this.services = List.of(services);
32+
this.services.addAll(Arrays.asList(services));
33+
}
34+
35+
@SafeVarargs
36+
public GrpcModule(Class<? extends BindableService>... serviceClasses) {
37+
bind(serviceClasses);
38+
}
39+
40+
@SafeVarargs
41+
public final GrpcModule bind(Class<? extends BindableService>... serviceClasses) {
42+
this.serviceClasses.addAll(List.of(serviceClasses));
43+
return this;
3744
}
3845

3946
@Override
4047
public void install(@NonNull Jooby app) throws Exception {
4148
var serverName = app.getName();
4249
var builder = InProcessServerBuilder.forName(serverName);
50+
final Map<String, MethodDescriptor<?, ?>> registry = new HashMap<>();
4351

4452
// 1. Register user-provided services
4553
for (var service : services) {
46-
builder.addService(service);
47-
for (var method : service.bindService().getMethods()) {
48-
var descriptor = method.getMethodDescriptor();
49-
String methodFullName = descriptor.getFullMethodName();
50-
registry.put(methodFullName, descriptor);
51-
String routePath = "/" + methodFullName;
52-
53-
//
54-
app.post(
55-
routePath,
56-
ctx -> {
57-
throw new IllegalStateException(
58-
"gRPC request reached the standard HTTP router for path: "
59-
+ routePath
60-
+ ". "
61-
+ "This means the native gRPC server interceptor was bypassed. "
62-
+ "Ensure you are running Jetty, Netty, or Undertow with HTTP/2 enabled, "
63-
+ "and that the GrpcProcessor SPI is correctly loaded.");
64-
});
65-
}
54+
bindService(app, builder, registry, service);
6655
}
6756

68-
this.grpcServer = builder.build().start();
69-
70-
// KEEP .directExecutor() here!
71-
// This ensures that when the background gRPC worker finishes, it instantly pushes
72-
// the response back to Undertow/Netty without wasting time on another thread hop.
73-
var channel = InProcessChannelBuilder.forName(serverName).directExecutor().build();
7457
var services = app.getServices();
75-
var bridge = new DefaultGrpcProcessor(channel, registry);
58+
var processor = new DefaultGrpcProcessor(registry);
59+
services.put(GrpcProcessor.class, processor);
60+
61+
// Lazy init service from DI.
62+
app.onStarting(
63+
() -> {
64+
for (Class<? extends BindableService> serviceClass : serviceClasses) {
65+
var service = app.require(serviceClass);
66+
bindService(app, builder, registry, service);
67+
}
68+
var grpcServer = builder.build().start();
7669

77-
// Register it in the Service Registry so the server layer can find it
78-
services.put(DefaultGrpcProcessor.class, bridge);
79-
services.put(GrpcProcessor.class, bridge);
70+
// KEEP .directExecutor() here!
71+
// This ensures that when the background gRPC worker finishes, it instantly pushes
72+
// the response back to Undertow/Netty without wasting time on another thread hop.
73+
var channel = InProcessChannelBuilder.forName(serverName).directExecutor().build();
74+
processor.setChannel(channel);
8075

81-
app.onStop(channel::shutdownNow);
82-
app.onStop(grpcServer::shutdownNow);
76+
app.onStop(channel::shutdownNow);
77+
app.onStop(grpcServer::shutdownNow);
78+
});
79+
}
80+
81+
private static void bindService(
82+
Jooby app,
83+
InProcessServerBuilder server,
84+
Map<String, MethodDescriptor<?, ?>> registry,
85+
BindableService service) {
86+
server.addService(service);
87+
for (var method : service.bindService().getMethods()) {
88+
var descriptor = method.getMethodDescriptor();
89+
String methodFullName = descriptor.getFullMethodName();
90+
registry.put(methodFullName, descriptor);
91+
String routePath = "/" + methodFullName;
92+
//
93+
app.post(
94+
routePath,
95+
ctx -> {
96+
throw new IllegalStateException(
97+
"gRPC request reached the standard HTTP router for path: "
98+
+ routePath
99+
+ ". "
100+
+ "This means the native gRPC server interceptor was bypassed. "
101+
+ "Ensure you are running Jetty, Netty, or Undertow with HTTP/2 enabled, "
102+
+ "and that the GrpcProcessor SPI is correctly loaded.");
103+
});
104+
}
83105
}
84106
}

modules/jooby-grpc/src/main/java/io/jooby/internal/grpc/DefaultGrpcProcessor.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,17 @@ public byte[] parse(InputStream stream) {
4747
}
4848

4949
private final Logger log = LoggerFactory.getLogger(getClass());
50-
private final ManagedChannel channel;
50+
private ManagedChannel channel;
5151
private final Map<String, MethodDescriptor<?, ?>> registry;
5252

53-
public DefaultGrpcProcessor(
54-
ManagedChannel channel, Map<String, MethodDescriptor<?, ?>> registry) {
55-
this.channel = channel;
53+
public DefaultGrpcProcessor(Map<String, MethodDescriptor<?, ?>> registry) {
5654
this.registry = registry;
5755
}
5856

57+
public void setChannel(ManagedChannel channel) {
58+
this.channel = channel;
59+
}
60+
5961
@Override
6062
public boolean isGrpcMethod(String path) {
6163
// gRPC paths typically come in as "/package.Service/Method"

modules/jooby-grpc/src/test/java/io/jooby/grpc/DefaultGrpcProcessorTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ public void setUp() {
5454
// The interceptor wraps the channel, but eventually delegates to the real one
5555
when(channel.newCall(any(MethodDescriptor.class), any(CallOptions.class))).thenReturn(call);
5656

57-
bridge = new DefaultGrpcProcessor(channel, registry);
57+
bridge = new DefaultGrpcProcessor(registry);
58+
bridge.setChannel(channel);
5859
}
5960

6061
@Test

tests/src/test/java/io/jooby/i3875/EchoGreeterService.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,17 @@
88
import io.grpc.stub.StreamObserver;
99

1010
public class EchoGreeterService extends GreeterGrpc.GreeterImplBase {
11+
12+
private final EchoService echoService;
13+
14+
@jakarta.inject.Inject
15+
public EchoGreeterService(EchoService echoService) {
16+
this.echoService = echoService;
17+
}
18+
1119
@Override
1220
public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
13-
HelloReply reply = HelloReply.newBuilder().setMessage("Hello " + req.getName()).build();
21+
HelloReply reply = HelloReply.newBuilder().setMessage(echoService.echo(req.getName())).build();
1422
responseObserver.onNext(reply);
1523
responseObserver.onCompleted();
1624
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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.i3875;
7+
8+
public class EchoService {
9+
10+
public String echo(String value) {
11+
return "Hello " + value;
12+
}
13+
}

tests/src/test/java/io/jooby/i3875/GrpcTest.java

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,18 @@
2323
import io.jooby.Jooby;
2424
import io.jooby.ServerOptions;
2525
import io.jooby.grpc.GrpcModule;
26+
import io.jooby.guice.GuiceModule;
2627
import io.jooby.junit.ServerTest;
2728
import io.jooby.junit.ServerTestRunner;
2829

2930
public class GrpcTest {
3031

3132
private void setupApp(Jooby app) {
33+
app.install(new GuiceModule());
34+
3235
app.install(
33-
new GrpcModule(
34-
new EchoGreeterService(),
35-
new EchoChatService(),
36-
ProtoReflectionServiceV1.newInstance()));
36+
new GrpcModule(new EchoChatService(), ProtoReflectionServiceV1.newInstance())
37+
.bind(EchoGreeterService.class));
3738
}
3839

3940
@ServerTest
@@ -50,9 +51,10 @@ void shouldHandleUnaryRequests(ServerTestRunner runner) {
5051

5152
try {
5253
var stub = GreeterGrpc.newBlockingStub(channel);
53-
var response = stub.sayHello(HelloRequest.newBuilder().setName("Edgar").build());
54+
var response =
55+
stub.sayHello(HelloRequest.newBuilder().setName("Pablo Marmol").build());
5456

55-
assertThat(response.getMessage()).isEqualTo("Hello Edgar");
57+
assertThat(response.getMessage()).isEqualTo("Hello Pablo Marmol");
5658
} finally {
5759
channel.shutdown();
5860
}
@@ -80,7 +82,9 @@ void shouldHandleDeadlineExceeded(ServerTestRunner runner) {
8082
var exception =
8183
org.junit.jupiter.api.Assertions.assertThrows(
8284
StatusRuntimeException.class,
83-
() -> stub.sayHello(HelloRequest.newBuilder().setName("Edgar").build()));
85+
() ->
86+
stub.sayHello(
87+
HelloRequest.newBuilder().setName("Pablo Marmol").build()));
8488

8589
// Assert that the bridge correctly caught the timeout and returned Status 4
8690
assertThat(exception.getStatus().getCode())
@@ -352,7 +356,7 @@ void shouldHandleMethodNotFound(ServerTestRunner runner) {
352356
channel,
353357
unknownMethod,
354358
io.grpc.CallOptions.DEFAULT,
355-
HelloRequest.newBuilder().setName("Edgar").build()));
359+
HelloRequest.newBuilder().setName("Pablo Marmol").build()));
356360

357361
// 3. Assert that Jooby's HTTP 404 is correctly translated by the gRPC client into
358362
// UNIMPLEMENTED

0 commit comments

Comments
 (0)