建立表單物件


在〈簡化控制器〉中的控制器已經簡化許多,當然,若要檢討的話,還有一些改善的空間,例如,在 AccountController 中,register 方法與 resetPassword 方法中,都有著針對表單的格式驗證,這類格式驗證可以抽取出來在表單物件中進行,從而簡化控制器的流程。

針對驗證的部份,JSR303 規範了 Java Validation API,而 Spring 可以整合 JSR303,然而需要有個 JSR303 的實作品,在這邊打算使用 Hibernate Validator,因此在 build.gradle 加入相依程式庫設定:

compile 'org.hibernate:hibernate-validator:6.0.13.Final'

接著先針對註冊表單設計一個對應的表單類別:

package cc.openhome.controller;

import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Email;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

public class RegisterForm {
    @Email(message = "未填寫郵件或格式不正確")
    private String email;

    @Pattern(regexp = "^\\w{1,16}$", message = "未填寫使用者名稱或格式不正確")
    private String username;

    @Size(min = 8, max = 16, message = "請確認密碼符合格式")
    private String password;

    private String password2;

    @AssertTrue(message="密碼與再次確認密碼不相符")
    private boolean isValid() {
        return password.equals(password2);
    }

    // ... Getters 與 Setters ... 略
}

RegisterForm 中,可以看到 @Email 用來驗證郵件格式、@Size 用來驗證字串長度以及 @Pattern 可自訂規則表示式進行驗證,至於 @AssertTrue 的部份,可自訂驗證方法來進行斷言,Spring 會自動收集對應名稱的請求參數,另外,針對重設密碼的表單,也設計了一個對應的表單物件:

package cc.openhome.controller;

import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Size;

public class ResetPasswordForm {
    private String token;
    private String name;
    private String email;

    @Size(min = 8, max = 16, message = "請確認密碼符合格式")
    private String password;

    private String password2;

    @AssertTrue(message="密碼與再次確認密碼不相符")
    private boolean isValid() {
        return password.equals(password2);
    }    


    // ... Getters 與 Setters ... 略
}

接下來就可以使用這兩個表單物件來重構 AccountController

package cc.openhome.controller;

...略

@Controller
@SessionAttributes("token")
public class AccountController {
    ... 略

    @PostMapping("register")
    public String register(
            @Valid RegisterForm form,
            BindingResult bindingResult,
            Model model) {

        List<String> errors = toList(bindingResult);

        String path;
        if(errors.isEmpty()) {
            path = REGISTER_SUCCESS_PATH;

            Optional<Account> optionalAcct = userService.tryCreateUser(
                    form.getEmail(), form.getUsername(), form.getPassword());
            if(optionalAcct.isPresent()) {
                emailService.validationLink(optionalAcct.get());
            } else {
                emailService.failedRegistration(
                    form.getUsername(), form.getEmail());
            }
        } else {
            path = REGISTER_FORM_PATH;
            model.addAttribute("errors", errors);
        }

        return path;
    }

    ...略

    @PostMapping("reset_password")
    public String resetPassword( 
            @Valid ResetPasswordForm form,
            BindingResult bindingResult,
            @SessionAttribute(name = "token") String storedToken,
            Model model) {

        if(storedToken == null || !storedToken.equals(form.getToken())) {
            return REDIRECT_INDEX_PATH;
        }

        List<String> errors = toList(bindingResult);

        if(!errors.isEmpty()) {
            Optional<Account> optionalAcct =
               userService.accountByNameEmail(form.getName(), form.getEmail());
            model.addAttribute("errors", errors);
            model.addAttribute("acct", optionalAcct.get());
            return RESET_PASSWORD_FORM_PATH;
        } else {
            userService.resetPassword(form.getName(), form.getPassword());
            return RESET_PASSWORD_SUCCESS_PATH;
        }    
    } 

    private List<String> toList(BindingResult bindingResult) {
        List<String> errors = new ArrayList<>(); 
        if(bindingResult.hasErrors()) {
            bindingResult.getFieldErrors().forEach(err -> {
                errors.add(err.getDefaultMessage());
            });
        }
        return errors;
    }    
}

register 現在注入了 RegisterForm 實例,@Valid 標註必須驗證欄位,如果有欄位驗證錯誤的話,會收集在 BindingResult 之中,透過注入其實例,稍後就可以檢查是否有相關的驗證問題。

BindingResult 可以透過 hasErrors 來詢問是否有欄位錯誤,如果有的話,可以透過 getFieldErrors 取得 FieldError 清單,透過每個 FieldError 實例的 getDefaultMessage 可以取得設定之錯誤訊息,如程式碼所示,resetPassword 方法也作了類似的處理。

你可以在 gossip 找到以上的範例專案。