2017-12-29

Tags: 程式語言 , java , spring

如何實作

spring 實作的 restful api,要針對輸入參數進行內容格式驗証,可分成下列儿類

針對 @RequestBody 進行 validation

實作程式碼重點如下

@RestController
@RequestMapping("/validateAnnotationDemo")
public class ValidateAnnotationDemoController {
    @RequestMapping(value = "/validateRequestBody", produces = MediaType.APPLICATION_JSON_VALUE)
    public User validateRequestBody(@Valid @RequestBody User user){
        ...etc;
    }

    private static class User{
        @NotBlank
        private String name;
        ...etc;
    }
}
@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity handleMethodArgumentNotValidException(HttpServletRequest req, MethodArgumentNotValidException e) {
        ...etc;
    }
}
  1. @RequestBody 前面要加上 @Valid
  2. pojo (在此例指 User class) 要加上 validation annotation (e.g. @NotBlank, @NotNull ...etc)
  3. validate fail 會丟出 MethodArgumentNotValidException,要自己寫 handler 決定哪些異常明細回傳給前端

針對 @RequestParam 進行 validation

實作程式碼重點如下

@Configuration
@ComponentScan
public class WebConfig {
    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        return new MethodValidationPostProcessor();
    }
}
@Validated
@RestController
@RequestMapping("/validateAnnotationDemo")
public class ValidateAnnotationDemoController {
    @RequestMapping(value = "/validateRequestParam", produces = MediaType.APPLICATION_JSON_VALUE)
    public Map<String,Object> validateRequestParam(@NotBlank(message = "message must not be blank") @RequestParam String message){
        ...etc;
    }
}
@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity handleConstraintViolationException(HttpServletRequest req, ConstraintViolationException e){
        ...etc;
    }
}
  1. 要注冊 MethodValidationPostProcessor bean,不然無法對 @RequestParam 進行 validation
  2. 在 @RestController 前面加上 @Validated
  3. 在 @RequestParam 前面加上 validation annotation (e.g. @NotBlank, @NotNull ...etc)
  4. validate fail 會丟出 ConstraintViolationException,要自己寫 handler 決定哪些異常明細回傳給前端

完整範例

WebConfig.java

@Configuration
@ComponentScan
public class WebConfig {

    /**
     * 注冊 MethodValidationPostProcessor bean 之後,Controller class 開頭加的 @Validated 與 method 裡針
     * 對 @RequestParam annotation 加的 validate annotation (e.g. @NotBlank, @NotNull ...etc)才會生效,
     * 沒注冊時就算程式碼裡有加這些 annotation 還是不會有作用
     *
     * @return
     */
    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        return new MethodValidationPostProcessor();
    }
}

ValidateAnnotationDemoController.java

@Validated
@RestController
@RequestMapping("/validateAnnotationDemo")
public class ValidateAnnotationDemoController {

    //在 class 開頭加了 @Validated 之後,針對 @RequestParam 加的 validation annotation (e.g. @NotBlank, @NotNull...etc) 才會生效
    @RequestMapping(value = "/validateRequestParam", produces = MediaType.APPLICATION_JSON_VALUE)
    public Map<String,Object> validateRequestParam(
            @NotBlank(message = "message must not be blank") @RequestParam String message){
        Map<String,Object> result = new HashMap<>();
        result.put("message", message);
        return result;
    }

    // 加對 @Valid 之後,會驗証 pojo (此例中指 User instance) 內容格式是否正確
    @RequestMapping(value = "/validateRequestBody", produces = MediaType.APPLICATION_JSON_VALUE)
    public User validateRequestBody(@Valid @RequestBody User user){
        return user;
    }

    private static class User{
        @NotBlank
        private String name;

        // 針對 list of pojo 的資料格式進行驗証,要加上 @NotEmpty, @Valid 二個 annotation
        @NotEmpty
        @Valid
        private List<ContactInfo> contactInfoList;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public List<ContactInfo> getContactInfoList() {
            return contactInfoList;
        }

        public void setContactInfoList(List<ContactInfo> contactInfoList) {
            this.contactInfoList = contactInfoList;
        }
    }

    private static class ContactInfo{
        @NotBlank
        private String address;

        public String getAddress() {
            return address;
        }

        public void setAddress(String address) {
            this.address = address;
        }
    }
}

GlobalExceptionHandler.java

/**
 * Controller 發生 uncatch exception 情況時,會統一在這個 class 被處理。
 */
@ControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(Exception.class)
    public ResponseEntity handleException(HttpServletRequest req, Exception e){
        logger.error(e.getMessage(), e);
        String errorMsg = (e.getMessage() == null) ? e.getClass().getSimpleName() : e.getMessage();
        Map<String,Object> error = Collections.singletonMap("error", errorMsg);
        return ResponseEntity.status(500).body(error);
    }

    /**
     * Controller 裡標注 @RequestParam 的變數在 validate fail 時會丟出 ConstraintViolationException。這個 method
     * 專門處理此類 exception
     *
     * @param req
     * @param e
     *
     * @return
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity handleConstraintViolationException(HttpServletRequest req, ConstraintViolationException e){
        logger.error(e.getMessage(), e);
        // "@NotBlank @RequestParam String myArg" 這樣的 validate 寫法在 validate fail 時無法得知 "哪個輸入參數名稱" 驗証失敗,這是 java reflection 本身的限制。
        // 用這類語法時要改寫成 "@NotBlank(myArg must not be blank) @RequestParam String myArg",程式裡的 validate annotation 要寫出 "完整出錯明細",
        // 不然在處理 ConstraintViolationException 時只會知道驗証失敗的原因,卻不知道是哪個輸入參數名稱驗証失敗。
        List<String> errorMessages = e.getConstraintViolations()
                .stream()
                .map(ConstraintViolation::getMessage)
                .collect(Collectors.toList());
        Map<String,Object> error = Collections.singletonMap("error", errorMessages);
        return ResponseEntity.status(400).body(error);
    }

    /**
     * Controller 裡標注 @RequestBody 的變數在 validate fail 時會丟出 MethodArgumentNotValidException。這個 method
     * 專門處理此類 exception
     *
     * @param req
     * @param e
     *
     * @return
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity handleMethodArgumentNotValidException(HttpServletRequest req, MethodArgumentNotValidException e) {
        logger.error(e.getMessage(), e);
        List<String> errorMessages = e.getBindingResult().getFieldErrors()
                .stream()
                .map(fieldError -> fieldError.getField() + " " + fieldError.getDefaultMessage()) // 記錄 "fieldName + validateFailMessage"
                .collect(Collectors.toList());
        Map<String,Object> error = Collections.singletonMap("error", errorMessages);
        return ResponseEntity.status(400).body(error);
    }
}

reference document