こんにちは。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 7.0.0.Final - Jakarta Bean Validation Reference Implementation: Reference Guide