こんにちは。ASKULのほかほかごはんです。
grpc-spring-boot-starter で Spring Validationがサポートされました。Version 4.3.0 から利用できます。
Spring BootでgRPCメッセージをValidateする方法についてはこれまでもprotoc-gen-validate などの選択肢がありましたが、使い慣れたBean Validationが利用できるのは嬉しいニュースです。
やってみる
Request/Response両方のValidationを定義できますが、今回の記事ではRequestのValidationを試してみます。 次の環境で検証します。
| Library | Version |
|---|---|
| Spring Boot | 2.4.2 |
| Java | 11.0.9 |
| Kotlin | 1.4.21 |
| grpc-spring-boot-starter | 4.4.3 |
事前準備
README に沿って実装します。
build.gradle.kts
spring-boot-starter-validationを追加します。
// 前略 dependencies { implementation("io.projectreactor.kotlin:reactor-kotlin-extensions") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") implementation("io.github.lognet:grpc-spring-boot-starter:4.4.3") implementation("org.springframework.boot:spring-boot-starter-validation") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") testImplementation("org.springframework.boot:spring-boot-starter-test") { exclude(group = "org.junit.vintage", module = "junit-vintage-engine") } testImplementation("io.projectreactor:reactor-test") } // 後略
spring-boot-starter-validationを追加することで、ValidatingInterceptor が
Global-InterceptorとしてDIコンテナに登録されます。
META-INF/validation.xml
validation.xmlを作成します。
<validation-config xmlns="http://xmlns.jcp.org/xml/ns/validation/configuration" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/validation/configuration http://xmlns.jcp.org/xml/ns/validation/configuration/validation-configuration-2.0.xsd" version="2.0"> <constraint-mapping>META-INF/validation/constraints-request.xml</constraint-mapping> <property name="hibernate.validator.fail_fast">false</property> </validation-config>
Validation定義はMETA-INF/validation/constraints-request.xmlに記述します。
試してみる
次のprotoでValidationを試します。
greeter.proto
syntax = "proto3";
option java_package = "io.grpc.examples";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
xmlにValidation Ruleを定義します。
constraints-request.xml
<constraint-mappings xmlns="http://xmlns.jcp.org/xml/ns/validation/mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/validation/mapping http://xmlns.jcp.org/xml/ns/validation/mapping/validation-mapping-2.0.xsd" version="2.0"> <bean class="io.grpc.examples.GreeterOuterClass$HelloRequest"> <getter name="name"> <constraint annotation="javax.validation.constraints.Size"> <groups> <value>org.lognet.springboot.grpc.validation.group.RequestMessage</value> </groups> <element name="min">0</element> <element name="max">3</element> </constraint> </getter> </bean> </constraint-mappings>
これで準備完了です。evans でテストしてみます。
Greeter@localhost:6565> call SayHello name (TYPE_STRING) => foobar command call: rpc error: code = InvalidArgument desc = name: 0 から 3 の間のサイズにしてください
Validateできました!
messageをcustomizeする
message tagを追加することでmessageを変更できます。
<constraint-mappings xmlns="http://xmlns.jcp.org/xml/ns/validation/mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/validation/mapping http://xmlns.jcp.org/xml/ns/validation/mapping/validation-mapping-2.0.xsd" version="2.0"> <bean class="io.grpc.examples.GreeterOuterClass$HelloRequest"> <getter name="name"> <constraint annotation="javax.validation.constraints.Size"> <message>{min}から{max}の間のサイズにしないとダメだぞ☆</message> <groups> <value>org.lognet.springboot.grpc.validation.group.RequestMessage</value> </groups> <element name="min">0</element> <element name="max">3</element> </constraint> </getter> </bean> </constraint-mappings>
Greeter@localhost:6565> call SayHello name (TYPE_STRING) => foobar command call: rpc error: code = InvalidArgument desc = name: 0から3の間のサイズにしないとダメだぞ☆
messageを変更できたぞ☆
自作Validatorを利用する
自作のAnnotationとValidatorを用意します。
@Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) @Constraint(validatedBy = [RequestValidator::class]) annotation class RequestConstraint( val message: String = "{request.validation.message}", val groups: Array<KClass<*>> = [], val payload: Array<KClass<out Payload>> = [] ) class RequestValidator : ConstraintValidator<RequestConstraint, GreeterOuterClass.HelloRequest> { override fun isValid( value: GreeterOuterClass.HelloRequest?, context: ConstraintValidatorContext? ): Boolean { val name = value?.name // hokahokagohanは認めない return name != "hokahokagohan" } }
作成したAnnotationをxmlに定義します。
<constraint-mappings xmlns="http://xmlns.jcp.org/xml/ns/validation/mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/validation/mapping http://xmlns.jcp.org/xml/ns/validation/mapping/validation-mapping-2.0.xsd" version="2.0"> <bean class="io.grpc.examples.GreeterOuterClass$HelloRequest"> <class> <constraint annotation="com.example.demo.validator.RequestConstraint"/> </class> </bean> </constraint-mappings>
ValidationMessages.propertiesにmessageを定義します。
request.validation.message=${validatedValue.name}君はダメだぞ☆
Greeter@localhost:6565> call SayHello
name (TYPE_STRING) => hokahokagohan
command call: rpc error: code = InvalidArgument desc = : hokahokagohan君はダメだぞ☆
Greeter@localhost:6565> call SayHello
name (TYPE_STRING) => foobar
{
"message": "Hello foobar"
}
Nested ClassをValidateする
requestにrepeated parameterをもつprotoをValidateします。
syntax = "proto3";
option java_package = "io.grpc.examples";
service Greeter {
rpc SayHelloToALotOfPeople (HelloRequests) returns (HelloReplies) {}
}
message HelloRequests {
repeated Name name = 1;
}
message Name {
string name = 1;
}
message HelloReplies {
repeated string message = 1;
}
xmlを定義します。<container-element-type><valid/></container-element-type>がポイントです。
<constraint-mappings xmlns="http://xmlns.jcp.org/xml/ns/validation/mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/validation/mapping http://xmlns.jcp.org/xml/ns/validation/mapping/validation-mapping-2.0.xsd" version="2.0"> <bean class="io.grpc.examples.GreeterOuterClass$HelloRequests"> <getter name="nameList"> <container-element-type> <valid/> </container-element-type> </getter> </bean> <bean class="io.grpc.examples.GreeterOuterClass$Name"> <getter name="name"> <constraint annotation="javax.validation.constraints.Size"> <groups> <value>org.lognet.springboot.grpc.validation.group.RequestMessage</value> </groups> <element name="min">0</element> <element name="max">3</element> </constraint> </getter> </bean> </constraint-mappings>
Greeter@localhost:6565> call SayHelloToALotOfPeople <repeated> name::name (TYPE_STRING) => foo <repeated> name::name (TYPE_STRING) => bar <repeated> name::name (TYPE_STRING) => foobar <repeated> name::name (TYPE_STRING) => fizzbuzz <repeated> name::name (TYPE_STRING) => command call: rpc error: code = InvalidArgument desc = nameList[2].name: 0 から 3 の間のサイズにしてください, nameList[3].name: 0 から 3 の間のサイズにしてください
まとめ
Bean Validationを使うことで簡単にgRPC MessageのValidationを実装できました。 今後もgRPCの記事を増やしていきたいと思います!
参考
GitHub - LogNet/grpc-spring-boot-starter: Spring Boot starter module for gRPC framework.
Hibernate Validator 8.0.2.Final - Jakarta Bean Validation Reference Implementation: Reference Guide