Skip to content

Commit defbc80

Browse files
author
Alexander Furer
committed
Spring Validation integration
1 parent 0d078e1 commit defbc80

File tree

18 files changed

+2070
-11
lines changed

18 files changed

+2070
-11
lines changed

README.adoc

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,10 @@ public class GreeterService extends GreeterGrpc.GreeterImplBase{
237237
// ommited
238238
}
239239
----
240+
=== Spring Validation support
241+
242+
The starter can be auto-configured to validate request/response gRPC service messages.
243+
Please continue to <<Implementing message validation>> for configuration details.
240244

241245
=== Spring security support
242246

@@ -307,6 +311,64 @@ If you enable both `NettyServer` and `in-process` servers, the `configure` metho
307311
If you need to differentiate between the passed `serverBuilder` s, you can check the type. +
308312
This is the current limitation.
309313

314+
== Implementing message validation
315+
316+
Thanks to https://beanvalidation.org/2.0/spec/[Bean Validation] configuration support via https://beanvalidation.org/2.0/spec/#xml[XML deployment] descriptor, one it's possible to
317+
provide the constraints for generated classes instead of instrumenting the generated messages with custom `protoc` compiler.
318+
319+
. Add `org.springframework.boot:spring-boot-starter-validation` dependency to your project.
320+
. Create `META-INF/validation.xml` and constraints declarations file(s). +
321+
See also https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/?v=6.1#chapter-xml-configuration[samples] from `Hibernate` validator documentation
322+
323+
You can find link:grpc-spring-boot-starter-demo/src/main/resources/META-INF/validation/constraints-message-to-validate.xml[demo configuration] and corresponding tests
324+
link:grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/ValidationTest.java[here]
325+
326+
Note, that both `request` and *response* messages are being validated.
327+
328+
If your gRPC method uses the same request and response message type, you can use `org.lognet.springboot.grpc.validation.group.RequestMessage` and
329+
`org.lognet.springboot.grpc.validation.group.ResponseMessage` validation groups to apply different validation logic :
330+
331+
[source,xml]
332+
----
333+
...
334+
<getter name="someField">
335+
336+
<!--should be empty for request message-->
337+
<constraint annotation="javax.validation.constraints.Size">
338+
<groups>
339+
<value>org.lognet.springboot.grpc.validation.group.RequestMessage</value> <1>
340+
</groups>
341+
<element name="min">0</element>
342+
<element name="max">0</element>
343+
344+
</constraint>
345+
346+
<!--should NOT be empty for response message-->
347+
<constraint annotation="javax.validation.constraints.NotEmpty">
348+
<groups>
349+
<value>org.lognet.springboot.grpc.validation.group.ResponseMessage</value> <2>
350+
</groups>
351+
</constraint>
352+
</getter>
353+
...
354+
----
355+
<1> Apply this constraint only for `request` message
356+
<2> Apply this constraint only for `response` message
357+
358+
359+
Note also custom cross-field link:grpc-spring-boot-starter-demo/src/main/java/org/lognet/springboot/grpc/demo/PersonConstraint.java[constraint] and its usage :
360+
361+
[source,xml]
362+
----
363+
<bean class="io.grpc.examples.GreeterOuterClass$Person">
364+
<class>
365+
<constraint annotation="org.lognet.springboot.grpc.demo.PersonConstraint"/>
366+
</class>
367+
...
368+
369+
</bean>
370+
----
371+
310372
== Spring Security Integration
311373

312374
=== Setup

grpc-spring-boot-starter-demo/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ dependencies {
2828
implementation "org.springframework.security:spring-security-config"
2929
implementation "org.springframework.security:spring-security-oauth2-jose"
3030
implementation "org.springframework.security:spring-security-oauth2-resource-server"
31+
implementation 'org.springframework.boot:spring-boot-starter-validation'
3132

3233

3334
implementation project(':grpc-spring-boot-starter')

grpc-spring-boot-starter-demo/src/main/java/org/lognet/springboot/grpc/demo/GreeterService.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,22 @@ public void sayAuthOnlyHello(Empty request, StreamObserver<GreeterOuterClass.Hel
4848

4949
sayAuthHello(request,responseObserver);
5050
}
51+
52+
@Override
53+
public void helloPersonValidResponse(GreeterOuterClass.Person request, StreamObserver<GreeterOuterClass.Person> responseObserver) {
54+
responseObserver.onNext(GreeterOuterClass.Person.newBuilder(request)
55+
.setNickName(request.getName().toLowerCase())
56+
.build());
57+
responseObserver.onCompleted();
58+
}
59+
60+
@Override
61+
public void helloPersonInvalidResponse(GreeterOuterClass.Person request, StreamObserver<GreeterOuterClass.Person> responseObserver) {
62+
responseObserver.onNext(GreeterOuterClass.Person.newBuilder(request)
63+
.clearNickName()
64+
.build());
65+
responseObserver.onCompleted();
66+
}
67+
68+
5169
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package org.lognet.springboot.grpc.demo;
2+
3+
import javax.validation.Constraint;
4+
import javax.validation.Payload;
5+
import java.lang.annotation.Documented;
6+
import java.lang.annotation.Retention;
7+
import java.lang.annotation.Target;
8+
9+
import static java.lang.annotation.ElementType.TYPE;
10+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
11+
12+
@Target({ TYPE})
13+
@Retention(RUNTIME)
14+
@Constraint(validatedBy = PersonValidator.class)
15+
@Documented
16+
public @interface PersonConstraint {
17+
18+
String message() default "{person.validation.message}";
19+
20+
Class<?>[] groups() default { };
21+
22+
Class<? extends Payload>[] payload() default { };
23+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package org.lognet.springboot.grpc.demo;
2+
3+
import io.grpc.examples.GreeterOuterClass;
4+
5+
import javax.validation.ConstraintValidator;
6+
import javax.validation.ConstraintValidatorContext;
7+
8+
public class PersonValidator implements ConstraintValidator<PersonConstraint, GreeterOuterClass.Person> {
9+
10+
11+
@Override
12+
public boolean isValid(GreeterOuterClass.Person value, ConstraintValidatorContext constraintContext) {
13+
14+
15+
if("bob".equalsIgnoreCase(value.getName())){
16+
return 20<value.getAge();
17+
}
18+
return true;
19+
20+
21+
}
22+
}

grpc-spring-boot-starter-demo/src/main/proto/greeter.proto

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,25 @@ service Greeter {
1010
rpc SayHello ( HelloRequest) returns ( HelloReply) {}
1111
rpc SayAuthHello ( google.protobuf.Empty) returns ( HelloReply) {}
1212
rpc SayAuthOnlyHello ( google.protobuf.Empty) returns ( HelloReply) {}
13+
rpc HelloPersonValidResponse ( Person) returns ( Person) {}
14+
rpc HelloPersonInvalidResponse ( Person) returns ( Person) {}
1315

1416
}
1517
service SecuredGreeter {
1618
rpc SayAuthHello ( google.protobuf.Empty) returns ( HelloReply) {}
1719

1820
}
1921

22+
message Person {
23+
string name = 1;
24+
uint32 age =2;
25+
Address address =3;
26+
string nickName=4;
27+
}
2028

29+
message Address{
30+
string city =1;
31+
}
2132
// The request message containing the user's name.
2233
message HelloRequest {
2334
string name = 1;

grpc-spring-boot-starter-demo/src/main/protoGen/io/grpc/examples/GreeterGrpc.java

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,68 @@ io.grpc.examples.GreeterOuterClass.HelloReply> getSayAuthOnlyHelloMethod() {
115115
return getSayAuthOnlyHelloMethod;
116116
}
117117

118+
private static volatile io.grpc.MethodDescriptor<io.grpc.examples.GreeterOuterClass.Person,
119+
io.grpc.examples.GreeterOuterClass.Person> getHelloPersonValidResponseMethod;
120+
121+
@io.grpc.stub.annotations.RpcMethod(
122+
fullMethodName = SERVICE_NAME + '/' + "HelloPersonValidResponse",
123+
requestType = io.grpc.examples.GreeterOuterClass.Person.class,
124+
responseType = io.grpc.examples.GreeterOuterClass.Person.class,
125+
methodType = io.grpc.MethodDescriptor.MethodType.UNARY)
126+
public static io.grpc.MethodDescriptor<io.grpc.examples.GreeterOuterClass.Person,
127+
io.grpc.examples.GreeterOuterClass.Person> getHelloPersonValidResponseMethod() {
128+
io.grpc.MethodDescriptor<io.grpc.examples.GreeterOuterClass.Person, io.grpc.examples.GreeterOuterClass.Person> getHelloPersonValidResponseMethod;
129+
if ((getHelloPersonValidResponseMethod = GreeterGrpc.getHelloPersonValidResponseMethod) == null) {
130+
synchronized (GreeterGrpc.class) {
131+
if ((getHelloPersonValidResponseMethod = GreeterGrpc.getHelloPersonValidResponseMethod) == null) {
132+
GreeterGrpc.getHelloPersonValidResponseMethod = getHelloPersonValidResponseMethod =
133+
io.grpc.MethodDescriptor.<io.grpc.examples.GreeterOuterClass.Person, io.grpc.examples.GreeterOuterClass.Person>newBuilder()
134+
.setType(io.grpc.MethodDescriptor.MethodType.UNARY)
135+
.setFullMethodName(generateFullMethodName(SERVICE_NAME, "HelloPersonValidResponse"))
136+
.setSampledToLocalTracing(true)
137+
.setRequestMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
138+
io.grpc.examples.GreeterOuterClass.Person.getDefaultInstance()))
139+
.setResponseMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
140+
io.grpc.examples.GreeterOuterClass.Person.getDefaultInstance()))
141+
.setSchemaDescriptor(new GreeterMethodDescriptorSupplier("HelloPersonValidResponse"))
142+
.build();
143+
}
144+
}
145+
}
146+
return getHelloPersonValidResponseMethod;
147+
}
148+
149+
private static volatile io.grpc.MethodDescriptor<io.grpc.examples.GreeterOuterClass.Person,
150+
io.grpc.examples.GreeterOuterClass.Person> getHelloPersonInvalidResponseMethod;
151+
152+
@io.grpc.stub.annotations.RpcMethod(
153+
fullMethodName = SERVICE_NAME + '/' + "HelloPersonInvalidResponse",
154+
requestType = io.grpc.examples.GreeterOuterClass.Person.class,
155+
responseType = io.grpc.examples.GreeterOuterClass.Person.class,
156+
methodType = io.grpc.MethodDescriptor.MethodType.UNARY)
157+
public static io.grpc.MethodDescriptor<io.grpc.examples.GreeterOuterClass.Person,
158+
io.grpc.examples.GreeterOuterClass.Person> getHelloPersonInvalidResponseMethod() {
159+
io.grpc.MethodDescriptor<io.grpc.examples.GreeterOuterClass.Person, io.grpc.examples.GreeterOuterClass.Person> getHelloPersonInvalidResponseMethod;
160+
if ((getHelloPersonInvalidResponseMethod = GreeterGrpc.getHelloPersonInvalidResponseMethod) == null) {
161+
synchronized (GreeterGrpc.class) {
162+
if ((getHelloPersonInvalidResponseMethod = GreeterGrpc.getHelloPersonInvalidResponseMethod) == null) {
163+
GreeterGrpc.getHelloPersonInvalidResponseMethod = getHelloPersonInvalidResponseMethod =
164+
io.grpc.MethodDescriptor.<io.grpc.examples.GreeterOuterClass.Person, io.grpc.examples.GreeterOuterClass.Person>newBuilder()
165+
.setType(io.grpc.MethodDescriptor.MethodType.UNARY)
166+
.setFullMethodName(generateFullMethodName(SERVICE_NAME, "HelloPersonInvalidResponse"))
167+
.setSampledToLocalTracing(true)
168+
.setRequestMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
169+
io.grpc.examples.GreeterOuterClass.Person.getDefaultInstance()))
170+
.setResponseMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
171+
io.grpc.examples.GreeterOuterClass.Person.getDefaultInstance()))
172+
.setSchemaDescriptor(new GreeterMethodDescriptorSupplier("HelloPersonInvalidResponse"))
173+
.build();
174+
}
175+
}
176+
}
177+
return getHelloPersonInvalidResponseMethod;
178+
}
179+
118180
/**
119181
* Creates a new async stub that supports all call types for the service
120182
*/
@@ -190,6 +252,20 @@ public void sayAuthOnlyHello(com.google.protobuf.Empty request,
190252
asyncUnimplementedUnaryCall(getSayAuthOnlyHelloMethod(), responseObserver);
191253
}
192254

255+
/**
256+
*/
257+
public void helloPersonValidResponse(io.grpc.examples.GreeterOuterClass.Person request,
258+
io.grpc.stub.StreamObserver<io.grpc.examples.GreeterOuterClass.Person> responseObserver) {
259+
asyncUnimplementedUnaryCall(getHelloPersonValidResponseMethod(), responseObserver);
260+
}
261+
262+
/**
263+
*/
264+
public void helloPersonInvalidResponse(io.grpc.examples.GreeterOuterClass.Person request,
265+
io.grpc.stub.StreamObserver<io.grpc.examples.GreeterOuterClass.Person> responseObserver) {
266+
asyncUnimplementedUnaryCall(getHelloPersonInvalidResponseMethod(), responseObserver);
267+
}
268+
193269
@java.lang.Override public final io.grpc.ServerServiceDefinition bindService() {
194270
return io.grpc.ServerServiceDefinition.builder(getServiceDescriptor())
195271
.addMethod(
@@ -213,6 +289,20 @@ public void sayAuthOnlyHello(com.google.protobuf.Empty request,
213289
com.google.protobuf.Empty,
214290
io.grpc.examples.GreeterOuterClass.HelloReply>(
215291
this, METHODID_SAY_AUTH_ONLY_HELLO)))
292+
.addMethod(
293+
getHelloPersonValidResponseMethod(),
294+
asyncUnaryCall(
295+
new MethodHandlers<
296+
io.grpc.examples.GreeterOuterClass.Person,
297+
io.grpc.examples.GreeterOuterClass.Person>(
298+
this, METHODID_HELLO_PERSON_VALID_RESPONSE)))
299+
.addMethod(
300+
getHelloPersonInvalidResponseMethod(),
301+
asyncUnaryCall(
302+
new MethodHandlers<
303+
io.grpc.examples.GreeterOuterClass.Person,
304+
io.grpc.examples.GreeterOuterClass.Person>(
305+
this, METHODID_HELLO_PERSON_INVALID_RESPONSE)))
216306
.build();
217307
}
218308
}
@@ -260,6 +350,22 @@ public void sayAuthOnlyHello(com.google.protobuf.Empty request,
260350
asyncUnaryCall(
261351
getChannel().newCall(getSayAuthOnlyHelloMethod(), getCallOptions()), request, responseObserver);
262352
}
353+
354+
/**
355+
*/
356+
public void helloPersonValidResponse(io.grpc.examples.GreeterOuterClass.Person request,
357+
io.grpc.stub.StreamObserver<io.grpc.examples.GreeterOuterClass.Person> responseObserver) {
358+
asyncUnaryCall(
359+
getChannel().newCall(getHelloPersonValidResponseMethod(), getCallOptions()), request, responseObserver);
360+
}
361+
362+
/**
363+
*/
364+
public void helloPersonInvalidResponse(io.grpc.examples.GreeterOuterClass.Person request,
365+
io.grpc.stub.StreamObserver<io.grpc.examples.GreeterOuterClass.Person> responseObserver) {
366+
asyncUnaryCall(
367+
getChannel().newCall(getHelloPersonInvalidResponseMethod(), getCallOptions()), request, responseObserver);
368+
}
263369
}
264370

265371
/**
@@ -302,6 +408,20 @@ public io.grpc.examples.GreeterOuterClass.HelloReply sayAuthOnlyHello(com.google
302408
return blockingUnaryCall(
303409
getChannel(), getSayAuthOnlyHelloMethod(), getCallOptions(), request);
304410
}
411+
412+
/**
413+
*/
414+
public io.grpc.examples.GreeterOuterClass.Person helloPersonValidResponse(io.grpc.examples.GreeterOuterClass.Person request) {
415+
return blockingUnaryCall(
416+
getChannel(), getHelloPersonValidResponseMethod(), getCallOptions(), request);
417+
}
418+
419+
/**
420+
*/
421+
public io.grpc.examples.GreeterOuterClass.Person helloPersonInvalidResponse(io.grpc.examples.GreeterOuterClass.Person request) {
422+
return blockingUnaryCall(
423+
getChannel(), getHelloPersonInvalidResponseMethod(), getCallOptions(), request);
424+
}
305425
}
306426

307427
/**
@@ -347,11 +467,29 @@ public com.google.common.util.concurrent.ListenableFuture<io.grpc.examples.Greet
347467
return futureUnaryCall(
348468
getChannel().newCall(getSayAuthOnlyHelloMethod(), getCallOptions()), request);
349469
}
470+
471+
/**
472+
*/
473+
public com.google.common.util.concurrent.ListenableFuture<io.grpc.examples.GreeterOuterClass.Person> helloPersonValidResponse(
474+
io.grpc.examples.GreeterOuterClass.Person request) {
475+
return futureUnaryCall(
476+
getChannel().newCall(getHelloPersonValidResponseMethod(), getCallOptions()), request);
477+
}
478+
479+
/**
480+
*/
481+
public com.google.common.util.concurrent.ListenableFuture<io.grpc.examples.GreeterOuterClass.Person> helloPersonInvalidResponse(
482+
io.grpc.examples.GreeterOuterClass.Person request) {
483+
return futureUnaryCall(
484+
getChannel().newCall(getHelloPersonInvalidResponseMethod(), getCallOptions()), request);
485+
}
350486
}
351487

352488
private static final int METHODID_SAY_HELLO = 0;
353489
private static final int METHODID_SAY_AUTH_HELLO = 1;
354490
private static final int METHODID_SAY_AUTH_ONLY_HELLO = 2;
491+
private static final int METHODID_HELLO_PERSON_VALID_RESPONSE = 3;
492+
private static final int METHODID_HELLO_PERSON_INVALID_RESPONSE = 4;
355493

356494
private static final class MethodHandlers<Req, Resp> implements
357495
io.grpc.stub.ServerCalls.UnaryMethod<Req, Resp>,
@@ -382,6 +520,14 @@ public void invoke(Req request, io.grpc.stub.StreamObserver<Resp> responseObserv
382520
serviceImpl.sayAuthOnlyHello((com.google.protobuf.Empty) request,
383521
(io.grpc.stub.StreamObserver<io.grpc.examples.GreeterOuterClass.HelloReply>) responseObserver);
384522
break;
523+
case METHODID_HELLO_PERSON_VALID_RESPONSE:
524+
serviceImpl.helloPersonValidResponse((io.grpc.examples.GreeterOuterClass.Person) request,
525+
(io.grpc.stub.StreamObserver<io.grpc.examples.GreeterOuterClass.Person>) responseObserver);
526+
break;
527+
case METHODID_HELLO_PERSON_INVALID_RESPONSE:
528+
serviceImpl.helloPersonInvalidResponse((io.grpc.examples.GreeterOuterClass.Person) request,
529+
(io.grpc.stub.StreamObserver<io.grpc.examples.GreeterOuterClass.Person>) responseObserver);
530+
break;
385531
default:
386532
throw new AssertionError();
387533
}
@@ -446,6 +592,8 @@ public static io.grpc.ServiceDescriptor getServiceDescriptor() {
446592
.addMethod(getSayHelloMethod())
447593
.addMethod(getSayAuthHelloMethod())
448594
.addMethod(getSayAuthOnlyHelloMethod())
595+
.addMethod(getHelloPersonValidResponseMethod())
596+
.addMethod(getHelloPersonInvalidResponseMethod())
449597
.build();
450598
}
451599
}

0 commit comments

Comments
 (0)