GenweiWu / Blog

个人技术能力提升
MIT License
4 stars 0 forks source link

Spring Validate #83

Open GenweiWu opened 2 years ago

GenweiWu commented 2 years ago

Spring Validation最佳实践

校验RequestBody参数

image

校验PathVariableRequestParam

image

GenweiWu commented 2 years ago

校验整数

Pattern不能用在int变量上 :joy:

//这样不行
    @Range(min = 1, max = 30, message = "timeout range invalid")
    @Pattern(regexp = "^[1-9][0-9]*$",message = "timeout is invalid")
    private int timeout=1 ;
No validator could be found for constraint 'javax.validation.constraints.Pattern' validating type 'java.lang.Integer'

这样也不行,因为 :joy:

//也不行
    @Range(min = 1, max = 30, message = "timeout range invalid")
    @Digits(integer = 2, fraction = 0)
    private int timeout = 1;

org.hibernate.validator.internal.constraintvalidators.bv.DigitsValidatorForNumber 转换为int时,11.3这种会转换为11,所以校验通过 即11.3校验通过,但是读取的数据还是11

if ( num instanceof BigDecimal ) {
bigNum = (BigDecimal) num;
}
else {
bigNum = new BigDecimal( num.toString() ).stripTrailingZeros();
}

不能直接用@Positive :joy:

@Positive是支持小数的,比如float、decimal都可以

GenweiWu commented 2 years ago

自定义校验:组合校验

可以参考:org.hibernate.validator.constraints.Range

例子

import org.hibernate.validator.constraints.Length;

import javax.validation.Constraint;
import javax.validation.OverridesAttribute;
import javax.validation.Payload;
import javax.validation.ReportAsSingleViolation;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import java.lang.annotation.*;

//这里有三个基本的校验组合在一起
@NotEmpty(message = "name is required")
@Length(max = 30, message = "name length is invalid")
@Pattern(regexp = "^[0-9a-zA-Z_\u4e00-\u9fa5]+$", message = "name character is invalid")

@Documented
@Constraint(validatedBy = {})   //这里的validatedBy为空就行
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@ReportAsSingleViolation
public @interface ValidateName {
    @OverridesAttribute(constraint = Length.class, name = "max") int max() default 30;   //还可以覆盖复合的校验注解的属性

    String message() default "name is invalid";

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

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

@ReportAsSingleViolation: 用来控制多个message的输出方式,这里只输出优先级高的message

@Documented
@Constraint(validatedBy = {})
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
//@ReportAsSingleViolation  //如果这里设置了该注解,则报错时返回的是1;如果没设置,则返回具体错误即2处
@URL(regexp = REGEX_IP, message = "endpoint url is invalid")  //2
@Length(max = 100, message = "endpoint length is invalid")   //2
public @interface ValidateEndpoint {
    @OverridesAttribute(constraint = Length.class, name = "max") int max() default 100;

    String message() default "endpoint is invalid";  //1

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

    Class<? extends Payload>[] payload() default {};
}
GenweiWu commented 2 years ago

自定义校验:枚举类型

枚举校验的注解


@Documented
@Constraint(validatedBy = {EnumValueStringValidator.class, EnumValueIntegerValidator.class})  //validatedBy要写上所有对应的校验类
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@NotNull(message = "Value cannot be null")
@ReportAsSingleViolation  //写上这个注解,则报错信息只会显示下方的"Value is not valid"
public @interface EnumValue {
Class<? extends java.lang.Enum<?>> enumClazz();

String message() default "Value is not valid";

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

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

}


> 针对字符串和Integer要分别写校验类
```java
public class EnumValueIntegerValidator implements ConstraintValidator<EnumValue, Integer> {

    private List<String> valueList = new ArrayList<>();

    @Override
    public void initialize(EnumValue constraintAnnotation) {
        Class<? extends Enum<?>> enumClazz = constraintAnnotation.enumClazz();

        Enum<?>[] enumConstants = enumClazz.getEnumConstants();
        for (Enum<?> enumConstant : enumConstants) {
            if (enumConstant instanceof EnumValidatable) {
                EnumValidatable enumValidatable = (EnumValidatable) enumConstant;
                valueList.add(enumValidatable.getValue());
            } else {
                valueList.add(enumConstant.name());
            }
        }

    }

    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) {
        return valueList.contains(String.valueOf(value));
    }
}
public class EnumValueStringValidator implements ConstraintValidator<EnumValue, String> {

    private List<String> valueList = new ArrayList<>();

    @Override
    public void initialize(EnumValue constraintAnnotation) {
        Class<? extends Enum<?>> enumClazz = constraintAnnotation.enumClazz();

        Enum<?>[] enumConstants = enumClazz.getEnumConstants();
        for (Enum<?> enumConstant : enumConstants) {
            if (enumConstant instanceof EnumValidatable) {
                EnumValidatable enumValidatable = (EnumValidatable) enumConstant;
                valueList.add(enumValidatable.getValue());
            } else {
                valueList.add(enumConstant.name());
            }
        }

    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
        return valueList.contains(value);
    }
}
GenweiWu commented 2 years ago

分组校验

1. 不太喜欢的风格:没使用默认分组

https://segmentfault.com/a/1190000024550434

@Data
public class UserForm {

    /**
     * id
     */
    @NotNull(message = "更新时id不能为空", groups = {Update.class})
    private String id;

    /**
     * 类型
     */
    @NotEmpty(message = "姓名不能为空" , groups = {Insert.class,Update.class})
    private String name;

    /**
     * 年龄
     */
    @NotEmpty(message = "年龄不能为空" , groups = {Insert.class,Update.class})
    private String age;

}
public interface Insert {
}

public interface Update {
}

/**

2. 使用默认分组(推荐!!!)

参考:https://www.jianshu.com/p/211aa556a4fa

@Data
public class UserForm {

    /**
     * id
     */
    @NotNull(message = "更新时id不能为空", groups = {Update.class})
    private String id;

    /**
     * 类型
     */
    @NotEmpty(message = "姓名不能为空")
    private String name;

    /**
     * 年龄
     */
    @NotEmpty(message = "年龄不能为空")
    private String age;

}
public interface Insert {
}

public interface Update {
}

/**

/**

GenweiWu commented 2 years ago

注解错误统一处理

@RestControllerAdvice
@Slf4j
public class ErrorCodeExceptionHandler {

    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
    public ErrorResponse invalidFormatHandler(MethodArgumentNotValidException e){
        String message = e.getBindingResult().getFieldErrors().stream().
                map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(";"));
        return new ErrorResponse(SYSTEM_ERROR, message);
    }

    @ExceptionHandler(value = ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
    public ErrorResponse invalidConstraintHandler(ConstraintViolationException e){
        List<String> msgList = new ArrayList<>();
        for (ConstraintViolation<?> constraintViolation : e.getConstraintViolations()){
            msgList.add(constraintViolation.getMessage());
        }
        String message = StringUtils.join(msgList.toArray(),",");
        return new ErrorResponse(SYSTEM_ERROR, message);
    }
}
GenweiWu commented 2 years ago

around注解在遇到valida注解+且校验失败时,无法触发aop的around方法

https://stackoverflow.com/questions/28975025/advise-controller-method-before-valid-annotation-is-handled

@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class RateLimitingAspect {

    @Autowired
    private RateLimitService rateLimitService;

    @Before("execution(* com.example..*.*(.., javax.servlet.ServletRequest+, ..)) " +
            "&& @annotation(com.example.RateLimited)")
    public void wait(JoinPoint jp) throws Throwable {

        ServletRequest request =
            Arrays
                .stream(jp.getArgs())
                .filter(Objects::nonNull)
                .filter(arg -> ServletRequest.class.isAssignableFrom(arg.getClass()))
                .map(ServletRequest.class::cast)
                .findFirst()
                .get();
        String ip = request.getRemoteAddr();
        int secondsToWait = rateLimitService.secondsUntilNextAllowedAttempt(ip);
        if (secondsToWait > 0) {
          throw new TooManyRequestsException(secondsToWait);
        }
    }

如果加了valid注解且校验失败,则上面的aop不会被触发

@RateLimited
@RequestMapping(method = RequestMethod.POST)
public HttpEntity<?> createAccount(
                           HttpServletRequest request,
                           @Valid @RequestBody CreateAccountRequestDto dto) {

...
}

规避方法

使用拦截器代替aop

@Component
public class RateLimitingInterceptor extends HandlerInterceptorAdapter {

    @Autowired
    private final RateLimitService rateLimitService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        if (HandlerMethod.class.isAssignableFrom(handler.getClass())) {
            rateLimit(request, (HandlerMethod)handler);
        }
        return super.preHandle(request, response, handler);
    }

    private void rateLimit(HttpServletRequest request, HandlerMethod handlerMethod) throws TooManyRequestsException {

        if (handlerMethod.getMethodAnnotation(RateLimited.class) != null) {
            String ip = request.getRemoteAddr();
            int secondsToWait = rateLimitService.secondsUntilNextAllowedInvocation(ip);
            if (secondsToWait > 0) {
                throw new TooManyRequestsException(secondsToWait);
            } else {
                rateLimitService.recordInvocation(ip);
            }
        }
    }
}
GenweiWu commented 2 years ago

校验集合不能为空

集合不能是null,或者为空集合 集合内容不能全是空,[" "]会报错,[" ","1"]有一个不为空则不会报错

class TestRequest{
    @NotEmpty(message = "ids is empty")
    private List<String> ids;
}
public void test(@RequestBody @Valid TestRequest testRequest)

校验集合数量

list至少包含1个元素,最多包含15个元素


public class Mock {
@Size(min=1, max=3)
private List<String> strings;

public List<String> getStrings() {
    return strings;
}

public void set(List<String> strings) {
    this.strings = strings;
}

}

GenweiWu commented 2 years ago

自定义类校验 (未实际尝试,待验证)

1. 实现validator接口

https://docs.spring.io/spring-framework/docs/3.2.x/spring-framework-reference/html/validation.html

@Component
public class UserValidator implements Validator {
    @Override
    public boolean supports(Class<?> clazz) {
        return User2.class.equals(clazz);
    }
    @Override
    public void validate(Object target, Errors errors) {
        ValidationUtils.rejectIfEmpty(errors, "name", "name.empty");
        User2 p = (User2) target;
        if (p.getId() == 0) {
            errors.rejectValue("id", "can not be zero");
        }
    }
}

2. 注册校验器

@RestController
public class UserController {
    @Autowired
    UserValidator validator;
    @InitBinder
    public void initBinder(WebDataBinder binder) {
        binder.setValidator(validator);
    }
    @RequestMapping(value = "/user/post", method = RequestMethod.POST)
    public ServiceResponse handValidatePost(@Validated @RequestBody User user) {
        ServiceResponse serviceResponse = new ServiceResponse();
        serviceResponse.setCode(0);
        serviceResponse.setMessage("test");
        return serviceResponse;
    }
}

参考

GenweiWu commented 2 years ago

自定义类级别校验

https://stackoverflow.com/a/2783859


@AddressAnnotation 
public class Address {
@NotNull @Max(50) private String street1;
@Max(50) private String street2;
@Max(10) @NotNull private String zipCode;
@Max(20) @NotNull String city;
@NotNull private Country country;
...

}

@Constraint(validatedBy = MultiCountryAddressValidator.class) @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface AddressAnnotation { String message() default ""; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; }

public class MultiCountryAddressValidator implements ConstraintValidator<AddressAnnotation, Address> { public void initialize(AddressAnnotation constraintAnnotation) { // initialize the zipcode/city/country correlation service }

/**
 * Validate zipcode and city depending on the country
 */
public boolean isValid(Address object, ConstraintValidatorContext context) {
    if (!(object instanceof Address)) {
        throw new IllegalArgumentException("@AddressAnnotation only applies to Address objects");
    }
    Address address = (Address) object;
    Country country = address.getCountry();
    if (country.getISO2() == "FR") {
        // check address.getZipCode() structure for France (5 numbers)
        // check zipcode and city correlation (calling an external service?)
        return isValid;
    } else if (country.getISO2() == "GR") {
        // check address.getZipCode() structure for Greece
        // no zipcode / city correlation available at the moment
        return isValid;
    }
    // ...
}

}


### 也可以自定义错误信息
> javax\validation\validation-api\2.0.1.Final\validation-api-2.0.1.Final-sources.jar!\javax\validation\ConstraintValidatorContext.java

```java
@Override
public boolean isValid(final Object value, final ConstraintValidatorContext context)
{
...

    //disable existing violation message
    context.disableDefaultConstraintViolation();
    //build new violation message and add it
    context.buildConstraintViolationWithTemplate(message).addConstraintViolation();
...
}