springmvc的入参校验、hibernate-validator、spring-boot-starter-validation、final-validator、手撸validator

使用validation验证,spring中可以选择spring-boot-starter-validationhibernate-validator 但我发现他们的使用不符日常开发习惯,无法直接自定义返回的检查异常。而且异常消息字段繁多、message、group,通常用不上。因此,我基于springmvc的源码,开发了 final-validator

gitee:https://gitee.com/lingkang_top/final-validator

github: https://github.com/xcocean/final-validato

常规的校验

常规的校验需要手动去验证参数是否存在,这种写法繁琐且不太优雅。

    @GetMapping("/get")
    public Object delete(Long id) {
        if (id == null)
            return new ResponseResult<>().fail("id不能为空");
        return new ResponseResult<>(classifyService.getById(id));
    }

使用validation验证

使用validation验证,spring中可以选择spring-boot-starter-validationhibernate-validator 但我发现他们的使用不符日常开发习惯,无法直接自定义返回的检查异常。而且异常消息字段繁多、message、group,通常用不上。

基于上面种种,我将试调springmvc的源码。从而实现自定义校验

源码试调 springboot3.2.2

基于springboot 3.2.2的spring-boot-starter-web进行试调,因为springmvc默认自带了入参对象的Validation

何时会启用验证?

请求时将会绑定入参,位于:ModelAttributeMethodProcessor.resolveArgument
断点可知ModelAttributeMethodProcessor.validateIfApplicable为验证入参注解等。
image-1706449949468

其中ModelAttributeMethodProcessor.validateIfApplicable比较有意思,它说明了要Valid开头的自定义注解才能开启校验,当然,原有的@jakarta.validation.Validorg.springframework.validation.annotation.Validated也能开启校验。
image-1706450367770

基于此,如果我们开发一个 validation 校验框架,那么开启校验的注解必须是以Valid开头的。
例如下面的

    @PostMapping("/add")
    public Object add(@Validated AddGoodsParam param){
        // ...
        return new ResponseResult<>().setData(param).setMsg("添加商品成功");
    }

上面的@Validated就是开启校验的注解了。

如何确定这个注解被校验?

回到上面的源码中,初始化数据绑定时

bindingResult = binderFactory.createBinder(webRequest, null, name).getBindingResult();

进入里面是一个初始化的创建:
image-1706450410996

在深入将看到关键初始化绑定:this.initializer.initBinder(dataBinder);
image-1706450493463

再次深入你将会开到它是如何判断入参是否需要设置校验支持
image-1706450563313

我们查看org.springframework.validation.Validator.supports(Class<?> clazz)的源码注释解释,可知道验证器能否提供验证支持。如果返回true就能支持验证,class就是对应上面的AddGoodsParam入参。

org.springframework.validation.Validator中的validate方法就是如何校验时调用的方法

image-1706450817958

由此可知,我们需要实现org.springframework.validation.Validator即可,回到org.springframework.web.bind.support.ConfigurableWebBindingInitializer中的public void initBinder(WebDataBinder binder)方法,查看validator是何时初始化的:

重新启动springboot,断点它的set方法即可看到初始化

image-1706450984528

注意,它初始化时入参是 WebMvcConfigurationSupport.NoOpValidator 私有静态类,点进去发现它啥也不做,相当于默认不校验:
image-1706451267780

继续跟进
image-1706451046485

它是由org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport.getConfigurableWebBindingInitializer初始化的

继续跟进
image-1706451106189
发现它是初始bean:RequestMappingHandlerAdapter 时初始化的。

总结,由上面可知,是否校验这个param入参,是根据org.springframework.validation.Validator的实现确定的。默认实现是 WebMvcConfigurationSupport.NoOpValidator 即为不校验。
org.springframework.validation.Validator实现的初始化,是根据RequestMappingHandlerAdapter这个bean初始化的。

那么如何利用springmvc开发一个validation?

1、创建一个springmvc项目

我这里使用springboot3.2.2
image-1706452285368
只勾选web,即spring-boot-starter-web

2、自定义一个开启校验注解

根据上面的试调,首先定义个 Valid 开头的注解,用于开启入参校验,当然你也可以直接使用@jakarta.validation.Valid@org.springframework.validation.annotation.Validated。这里我们自定义,不需要他们的花里胡哨功能:

import java.lang.annotation.*;

/**
 * @author lingkang
 * created by 2024/1/28
 * 开启需要检查的入参对象
 */
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ValidObject {
}

3、编写一个判空注解

import java.lang.annotation.*;

/**
 * @author lingkang<br/>
 * created by 2024/1/28<br/>
 * 注解的属性必定不为空、不为空格字符<br/>
 * 默认返回 {字段名称}不能为空
 */
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NotBlank {

    /**
     * 校验失败时返回的消息,返回例示 message
     */
    String message() default "";

    /**
     * 校验失败时返回的消息,优先级比 message 高,返回例示 {tag}不能为空
     */
    String tag() default "{message} 不能为空";
}

4、编写一个需要校验的入参

例如登录的入参:LoginParam ,并注解上我们自定义的@NotBlank

/**
 * @author lingkang
 * Created by 2024/1/28
 */
public class LoginParam {
    @NotBlank
    private String username;
    @NotBlank
    private String password;
    
    // get 、set 实现
}

5、定义个异常用来特别捕获

/**
 * @author lingkang
 * Created by 2024/1/28
 */
public class ValidatedException extends RuntimeException{
    public ValidatedException(String message) {
        super(message);
    }
}

6、自定义实现 org.springframework.validation.Validator

实现就叫MyValidatorImpl

import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import top.lingkang.validationspringmvc.param.LoginParam;

import java.lang.reflect.Field;

/**
 * @author lingkang
 * Created by 2024/1/28
 */
public class MyValidatorImpl implements Validator {
    @Override
    public boolean supports(Class<?> clazz) {
        if (clazz.isAssignableFrom(LoginParam.class))
            return true;
        return false;
    }

    @Override
    public void validate(Object target, Errors errors) {
        // 若已经积累了异常,将其抛出
        if (errors.hasErrors()) {
            throw new RuntimeException("校验参数前存在mvc解析异常" + errors);
        }
        // 不是我们自定义的登录入参时不校验
        if (!target.getClass().isAssignableFrom(LoginParam.class))
            return;
        Field[] fields = target.getClass().getDeclaredFields();
        for (Field field : fields) {
            NotBlank notBlank = field.getAnnotation(NotBlank.class);
            if (notBlank != null) {// 存在 @NotBlank 注解时校验它的值是否为空!
                field.setAccessible(true);
                Object o = null;
                try {
                    o = field.get(target);
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                }
                // 在此处执行判空逻辑,我这里随便判空一下,非NotBlank的判空方式
                if (o == null || o.toString().length() == 0) {
                    String tag = notBlank.tag();
                    tag = tag.replace("{message}", field.getName());// 将tag的值替换
                    throw new ValidatedException(tag);// 将异常抛出
                }
            }
        }
    }
}

7、配置 Validator

我们如何把我们的校验替换 WebMvcConfigurationSupport.NoOpValidator 呢?将配置替换RequestMappingHandlerAdapter中的org.springframework.validation.Validator即可

有多种方式配置,下面我以一个简单的方式实现:

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;

/**
 * @author lingkang
 * Created by 2024/1/28
 */
@Configuration
public class ValidatedConfig {
    @Bean
    public MyValidatorImpl myValidator(@Qualifier("requestMappingHandlerAdapter")RequestMappingHandlerAdapter requestMappingHandlerAdapter){
        MyValidatorImpl myValidator=new MyValidatorImpl();
        // 配置我们的 Validator 实现
        ConfigurableWebBindingInitializer initializer = (ConfigurableWebBindingInitializer) requestMappingHandlerAdapter.getWebBindingInitializer();
        initializer.setValidator(myValidator);
        return myValidator;
    }
}

8、编写一个异常捕获,用于返回自定义的错误

import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.Map;

/**
 * @author lingkang
 * Created by 2024/1/28
 */
@RestControllerAdvice
public class ErrorConfig {
    private static final Logger log = LoggerFactory.getLogger(ErrorConfig.class);

    @ExceptionHandler(ValidatedException.class)
    public Object validatedException(ValidatedException e, HttpServletRequest request) {
        log.warn("入参校验异常:{} {} {}", request.getMethod(), request.getRequestURL(), e.getMessage());
        Map<String, Object> result = new HashMap<>();
        result.put("code", 1);
        result.put("msg", e.getMessage());
        return result;
    }
}

9、调用实现校验

编写一个接口:

@RestController
public class WebController {
    // @ValidObject 注解将启用校验吗,一定要记得添加
    @RequestMapping("/login")
    public Object login(@ValidObject LoginParam param) {
        return param;
    }
}

http调用:http://localhost:8080/login
image-1706454072908

http调用:http://localhost:8080/login?username=lk
image-1706454093833

http调用:http://localhost:8080/login?username=lk&password=lingkang
image-1706454134339

报错截图:
image-1706454746116

10、项目截图

image-1706454796063

至此,validator底层实现完成。我开发的 final-validator 框架就是基于此实现,更多细节你可以自行实现:

gitee:https://gitee.com/lingkang_top/final-validator

github: https://github.com/xcocean/final-validato

看到这里,希望帮我点点 start,感谢!