IK.AM

@making's tech note


Spring 6.1でYAVIがSpring MVCの@Valid/Validatedで使いやすくなった

🗃 {Programming/Java/org/springframework/validation}
🏷 Java 🏷 YAVI 🏷 Spring Boot 🏷 Spring MVC 
🗓 Updated at 2023-07-10T07:35:31Z  🗓 Created at 2023-07-10T07:16:44Z   🌎 English Page

Spring 6.1.0-M1でFunctionalなValidator factory methodが追加されました (https://github.com/spring-projects/spring-framework/pull/29890) 。

これにより、YAVIのValidatorからSpringのValidatorへの変換が容易になりました。そのため、@Valid/@ValidatedでValidationを行なっているControllerでYAVIが使いやすくなりました。

Springのガイドのうち"Validating Form Input" (https://spring.io/guides/gs/validating-form-input/) を例にBean ValidationをYAVIに変更してみます。

まずはValidationの定義。

元の定義は

package com.example.validatingforminput;

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

public class PersonForm {

    @NotNull
    @Size(min=2, max=30)
    private String name;

    @NotNull
    @Min(18)
    private Integer age;

    // ...
}

です。

YAVIで書くと、例えば

package com.example.validatingforminput;

import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.Validator;

public class PersonForm {

    public static Validator<PersonForm> validator = ValidatorBuilder.<PersonForm>of()
            .constraint(PersonForm::getName, "name", c -> c.notNull().greaterThanOrEqual(2).lessThanOrEqual(30))
            .constraint(PersonForm::getAge, "age", c -> c.notNull().greaterThanOrEqual(18))
            .build();

    // ...
}

になります。

ControllerでのValidationの利用は、元のコードは

package com.example.validatingforminput;

import jakarta.validation.Valid;

import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


@Controller
public class WebController implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/results").setViewName("results");
    }

    @GetMapping("/")
    public String showForm(PersonForm personForm) {
        return "form";
    }

    @PostMapping("/")
    public String checkPersonInfo(@Valid PersonForm personForm, BindingResult bindingResult) {

        if (bindingResult.hasErrors()) {
            return "form";
        }

        return "redirect:/results";
    }
}

です。

YAVI + Spring 6.1を使った場合、次のように書けます。

package com.example.validatingforminput;

import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


@Controller
public class WebController implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/results").setViewName("results");
    }

    @InitBinder
    void initBinder(WebDataBinder binder) {
        Validator personValidator = Validator.forInstanceOf(PersonForm.class, PersonForm.validator.toBiConsumer(Errors::rejectValue));
        binder.addValidators(personValidator);
    }

    @GetMapping("/")
    public String showForm(PersonForm personForm) {
        return "form";
    }

    @PostMapping("/")
    public String checkPersonInfo(@Validated PersonForm personForm, BindingResult bindingResult) {

        if (bindingResult.hasErrors()) {
            return "form";
        }

        return "redirect:/results";
    }
}

以下の部分がSpring 6.1で利用可能になったfactory methodを利用しています。

Validator personValidator = Validator.forInstanceOf(PersonForm.class, PersonForm.validator.toBiConsumer(Errors::rejectValue));

上記の例ではBean Validationおよびspring-boot-starter-validationを依存関係から除外しています。そのため@Valid (Bean Validationのアノテーション)の代わりに@Validated (Springのアノテーション)を使用しています。どちらも利用可能です。

全体のDiffは https://github.com/making/gs-validating-form-input/commit/7261b26bf94c4aae86b52c68f9f78380e07a79f3 です。

ControllerだけのDiffは次の図の通りです。

image

Controllerのコードは大きく変えることなくBean Validationの代わりにYAVIを使用することが可能になりました。


ちなみにドキュメントに記載されている通り、これまでもYAVIはSpring MVCで容易に利用可能です。

今回の例で言うと以下のように書けます。

    @PostMapping("/")
    public String checkPersonInfo(@Validated PersonForm personForm, BindingResult bindingResult) {
        ConstraintViolations violations = PersonForm.validator.validate(personForm);
        if (!violations.isValid()) {
            violations.apply(bindingResult::rejectValue);
            return "form";
        }

        return "redirect:/results";
    }

or

    @PostMapping("/")
    public String checkPersonInfo(@Validated PersonForm personForm, BindingResult bindingResult) {
        return PersonForm.validator.applicative()
                .validate(personForm)
                .fold(violations -> {
                    ConstraintViolations.of(violations).apply(bindingResult::rejectValue);
                    return "form";
                }, form -> "redirect:/results");
    }

今回の更新は@Valid/@Validatedによるプログラミングモデルが好きな場合に特に親和性が高くなったという話でした。


✒️️ Edit  ⏰ History  🗑 Delete