gRPC Request MessageをBean ValidationでValidateする

こんにちは。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

ASKUL Engineering BLOG

2021 © ASKUL Corporation. All rights reserved.