在〈簡化控制器〉中的控制器已經簡化許多,當然,若要檢討的話,還有一些改善的空間,例如,在 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 找到以上的範例專案。