Custom validation trong spring boot

1. Mở đầu

Bạn cũng biết khi chúng ta code api thì một công đoạn bắt buộc đó là phải validate input

Trong Bean Validation 2.0 framework đã support rất nhiều annotation hỗ trợ việc validate input thế nhưng những anotaion đó cũng không thể validate cho tất cả các trường hợp, cũng may mà chúng ta có customize các annotation theo ý muốn.

2. Custom valiation

Để thực hiện việc này tôi sẽ create một annotaion có thể validate cho trường hợp hai field truyền phải equals nhau. tôi đặt annotaion này là FieldsValueMatch

Đầu tiên chúng ta cần tạo inteface cho annotaion này.

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.Documented;

@Constraint(validatedBy = FieldsValueMatchValidator.class)
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FieldsValueMatch {

    String message() default "Fields values don't match!";

    String field();

    String fieldMatch();

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

    Class<? extends Payload>[] payload() default {};

    @Target({ ElementType.FIELD })
    @Retention(RetentionPolicy.RUNTIME)
    @interface List {
        FieldsValueMatch[] value();
    }
}

Bạn hay chú ý vào @Constraint(validatedBy = FieldsValueMatchValidator.class)

FieldsValueMatchValidator.class là class để implement cái logic validate

Đây là FieldsValueMatchValidator.class

public class FieldsValueMatchValidator
        implements ConstraintValidator<FieldsValueMatch, Object> {

    private String field;
    private String fieldMatch;

    @Override
    public void initialize(FieldsValueMatch constraintAnnotation) {
        this.field = constraintAnnotation.field();
        this.fieldMatch = constraintAnnotation.fieldMatch();
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {

        Object fieldValue = new BeanWrapperImpl(value)
                .getPropertyValue(field);
        Object fieldMatchValue = new BeanWrapperImpl(value)
                .getPropertyValue(fieldMatch);

        if (fieldValue != null) {
            return fieldValue.equals(fieldMatchValue);
        } else {
            return fieldMatchValue == null;
        }
    }

}

vậy là chúng ta đã create annotaion bây giờ cần handle exception cho annotation

Đối với các exception này nó sẽ thuộc loại MethodArgumentNotValidException

public class CustomizedResponseEntityExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public final ResponseEntity<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
        Map<String, String> errorMessageMap = new HashMap<>();

        ex.getBindingResult().getAllErrors().forEach(e -> {
            if (e instanceof FieldError) {
                errorMessageMap.put(((FieldError) e).getField(), e.getDefaultMessage());
            } else {
                
errorMessageMap.put((String) ReflectionUtils.getPropertyValue(Objects.requireNonNull(e.getArguments())[2], CommonConst.RESOLVABLE_STRING), e.getDefaultMessage());
                  
            }

        });

        ErrorObject errorObject = new ErrorObject();
        errorObject.setMessages(errorMessageMap);

        setLogDebug(errorObject, ex.getStackTrace());

        return new ResponseEntity<>(errorObject, HttpStatus.BAD_REQUEST);
    }

	
  //... other exception 

}

public class ReflectionUtils {
     public static <T> Object getPropertyValue(T obj, String fieldName) {
        Object result = null;
        try {
            Field field = obj.getClass().getDeclaredField(fieldName);
            field.setAccessible(true);
            result = field.get(obj);
        } catch (NoSuchFieldException | IllegalAccessException ignored) {
        }

        return result;
    }
		
}

Oke đã xong bây giờ bạn có thể test thành quả rồi

Tạo một Dto ví dụ TestDto và sử dụng annotation FieldsValueMatch để validate

@FieldsValueMatch(
        field = "password",
        fieldMatch = "passwordConfirmation",
        message = "Password confirmation is not match password"
)
public class TestDto {
    @NotEmpty(message = "password can not empty")
    private String password;
    private String passwordConfirmation;
}

Tạo một controller để test

package com.ssk.sskbe.appweb.controller;


import com.ssk.sskbe.appweb.dto.core.TestDto;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;

@RestController
@RequestMapping("/test")
public class TestController {
    @PostMapping
    public String test(@Valid @RequestBody TestDto testDto) {

        return "success";
    }
}

Oke vậy bây giờ call thử api xem với trường hợp password và passwordConfim không khớp nhau

curl -X POST \
  http://localhost:8080/test \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -H 'Postman-Token: 5f3bf0c7-01cf-41ab-b807-89bfdec5f2c4' \
  -d '{

	"password":"[email protected]",
	"passwordConfirmation":"[email protected]"
}'

kết quả

{
    "code": null,
    "messages": {
        "passwordConfirmation": "Password confirmation is not match password"
    },
    "forceMessage": null,
    "debug": nul
}

3. Kết

Đây là một hướng dẫn đơn giản để customize một annotaion để validate

Hi vọng rằng qua bài viết này bạn có hiểu và tự customize các annotaion riêng cho dự án của mình

Cảm ơn các bạn đã đọc và hẹn gặp lại trong bài viết tiếp theo.