作記録

記憶代わり

Spring Frameworkを利用したPOST HTTP Requestの実装例

本記事で利用する技術のVersion

  • Open JDK 11.0.9
  • Spring Framework Boot 2.5.3
  • Spring Boot Starter Web 2.5.3
  • Spring Boot Starter Validation 2.5.3
  • Spring Boot Starter Thymeleaf 2.5.3

実装例

1. Controller

@Controller
@RequestMapping("/group")
public class GroupMakingController {
    GroupMakingService groupMakingService;

    GroupMakingController(GroupMakingService groupMakingService) {
        this.groupMakingService = groupMakingService;
    }

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        binder.setAllowedFields("username.value");
    }

    @GetMapping("/making")
    public String グループ作成画面(@ModelAttribute("groupMakingForm") GroupMakingForm groupMakingForm) {
        return "group/making/form";
    }

    @PostMapping("/making")
    public String グループ作成(@Validated GroupMakingForm groupMakingForm, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return "group/making/form";
        }

        if (groupMakingService.確認(groupMakingForm.groupName()) {
            bindingResult.rejectValue(
                    "username.value", "Duplicate", "既に作成されたグループ名の為、作成出来ません。"
            );
            return "group/making/form";
        } 

        groupMakingService.記録(groupMakingForm.groupName());

        return "redirect:/group/complete";
    }
}

●1-1. WebDataBinder::setAllowedFields

WebDataBinderの親クラスであるDataBinderの説明に、下記の説明がある為指定している。

DataBinder で allowedFields プロパティを指定することを強くお勧めします。

●1-2. グループ作成画面メソッド

HTMLをレスポンスするメソッドの引数に@ModelAttributeとHTMLで利用したいモデルのクラスを指定する。

●1-3. グループ作成メソッド

POSTを処理するメソッドの引数に@Validatedと上記1-1で指定したモデルのクラスとBindingResultを引数に指定する。

・1-3-1. @Validatedと@Validの違い

@Validatedは、Spring Frameworkパッケージ配下にあるアノテーションSpring FrameworkのControllerで利用する。

@Validは、javax.validationパッケージ配下にあるアノテーション。ネストしたオブジェクトに対して付与する事でバリデーションの検証時にネストしたオブジェクトに対して検証を行うようになる。

・1-3-2. BindingResult::hasErrors()

GroupMakingForm(@Validatedを付与した上記1-2で指定したモデルのクラス)のバリデーションの検証の結果、違反がある場合BindingResult::hasErrors()がtrueを返す。

Thymeleafは上記1-1で指定したモデルを利用する事が出来る。

・1-3-3. BindingResult::rejectValue()

BindingResult::addError()を使うと、POSTの処理時に作られていたGroupMakingFormのモデルが初期化される。つまり、バリデーションエラーで元の画面をレスポンスする際に入力が消えてしまう。

2. Form

public class GroupMakingForm {
    @Valid
    Username username;

    public GroupMakingForm() {}

    public Username getUsername() {
        return username;
    }

    public GroupName groupName() {
        return username.groupName();
    }

    public void setUsername(Username username) {
        this.username = username;
    }
}
public class Username {
    @NotBlank
    @Size(max = 40, message = "グループ名は40文字以下で入力してください")
    String value;

    public Username(String username) {
        this.value = username;
    }

    public Username() {
    }

    public GroupName groupName() {
        return new GroupName(this.value);
    }

    @Override
    public String toString() {
        return super.toString();
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }
}

●2-1. デフォルトコンストラクタとGetterとSetter

Spring FrameworkとThymeleafの為に必要っぽい。

・2-1-1. デフォルトコンストラク

Spring Frameworkがこのモデルを初期化する際に必要っぽい。

・2-1-2. Getter

Thymeleafがこのモデルの値を参照する際に必要っぽい。

・2-1-3. Setter

Spring Frameworkがこのモデルの値をsetする際に必要っぽい。

3. @NotBlank

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = NotBlankValidator.class)
public @interface NotBlank {
    String message() default "入力してください";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}
public class NotBlankValidator implements ConstraintValidator<NotBlank, String> {
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (Objects.isNull(value)) return false;

        return value.strip().length() > 0;
    }
}

javax.validationパッケージには、@NotBlankがある。

このアノテーションは、null, 空文字("")、半角スペース(" ") に対しては、検証の結果エラーを示すが、全角スペース(" ")にはエラーを示さない。

よって、上記のように全角スペース(" ")にエラーを示す@NotBlankを用意する。

4. group/making/form.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>グループ作成</title>
</head>
<body>
<form method="post" th:action="@{/group/making}" th:object="${groupMakingForm}">
    <input th:classappend="${#fields.hasErrors('username.value')} ? 'input-error' : ''" type="text" id="username" th:field="*{username.value}" autofocus />
    <div class="error" th:if="${#fields.hasErrors('username.value')}" th:errors="*{username.value}"></div>
    <button>作る</button>
</form>
</body>
</html>

●4-1. モデルのfiledへの参照

username.value のようにGroupMakingFormにもUsernameにもgetterが実装されている為、field名で参照出来る。

●4-2. fields.hasErrors()

${#fields.hasErrors(username.value)} のようにfieldを指定する事で、そのfieldのバリデーションのエラーがあるかを判断出来る。

●4-3. th:error

th:errors="*{username.value}" のようにfieldを指定する事で、BindingResultにより生成されるObjectErrorモデルからエラーメッセージを参照出来る。