你一定有經驗在網頁的表單輸入錯誤資訊時旁邊出現紅字提示,比如說在輸入身分證字號時輸入不符合規則的身分證字號,又或者輸入了一個不可能存在的生日資訊。出現紅字代表他有做資料驗證,前端驗證可以減輕後端驗證的負擔,後端驗證則是為系統接收資料的最後一道重要關卡,今日我們就來看看Spring MVC如何處理這一塊。
(1) 請參考Day27 module
(2) 使用JSON相關設置請參考Day29
Java最早的JSR-303 Bean Validation就是來做資料驗證的規範,目前Bean Validataion的版本已到3.0,對應目前穩定版本的實作是Hibernate Validator8.0
reference:https://hibernate.org/validator/releases/
我們使用3.0的規範我們從官網知道Jakarta Bean Validation是屬於Java EE的規格而hibernate提供了實作,按照官網的提供的maven資訊引入對應的jar
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>8.0.1.Final</version>
</dependency>
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator-annotation-processor</artifactId>
    <version>8.0.1.Final</version>
</dependency>
透過此註解啟用驗證,就會對設定好的bean進行驗證
會將此驗證的結果封裝到BindingResult物件
Employee.java
public class Employee {
    @NotNull
    private String empId;
    
    //透過message可以自訂義錯誤返回值
    @NotEmpty(message = "員工姓名不為空")
    private String name;
    @Email
    @NotNull
    private String email;
    @Min(0)
    @Max(110)
    @NotNull
    private int age;
    //getter and setter省略
}
可以透過BindingResult取得驗證不通過的訊息再進行封裝返回前端
@Controller
public class DemoValidateController {
    @ResponseBody
    @PostMapping("SaveEmp")
    public String DemoValidate(@RequestBody @Validated Employee emp, BindingResult result) {
        result.getFieldErrors().forEach(err->{
            System.out.println(err.getField()+":"+err.getDefaultMessage());
        });
        return result.toString();
    }
}
postman test
backend result
以下就試寫一個性別的驗證器
implements ConstraintValidator並覆寫isValid方法
public class GenderValidator implements ConstraintValidator<Gender, String> {
    @Override
    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        return s.equals("男")||s.equals("女");
    }
}
這邊可以去找一個Validation的annotation來複製改寫
@Documented
//這邊要加入自己寫的Validator
@Constraint(validatedBy = {GenderValidator.class})
//這邊定義此annotation可以放置在那些地方
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Gender {
    String message() default "{性別不對喔}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}
Employee加入gender屬性
@Gender
private String gender;
postman test
backend result
如果在每一個Controller的每一個方法中都需要自己去處理校驗結果BindingResult是不是很累人呢,此時前一天介紹的Exception Handling來解決這個問題。當沒有使用BindingResult去接驗證結果時,驗證失敗會拋出MethodArgumentNotValidException,並且可以從該Exception取出BindingResult,是不是很棒呢
於DemoValidateController再創建一個方法
@ResponseBody
@PostMapping("SaveEmployee")
public ResponseEntity<String> DemoValidateWithExceptionHandling(@RequestBody @Validated Employee emp) {
    System.out.println("Access SaveEmployee...");
    return ResponseEntity.ok("Save Employee success!!!");
}
創建GlobalExceptionHandler
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Map<String, String> HandleException(MethodArgumentNotValidException ex){
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        return errors;
    }
}
