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

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

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

gitee:gitee.com/lingkang_to...

github: github.com/xcocean/fin...

常规的校验

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

java 复制代码
    @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为验证入参注解等。

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

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

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

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

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

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

java 复制代码
bindingResult = binderFactory.createBinder(webRequest, null, name).getBindingResult();

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

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

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

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

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

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

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

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

继续跟进

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

继续跟进

发现它是初始bean:RequestMappingHandlerAdapter 时初始化的。

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

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

1、创建一个springmvc项目

我这里使用springboot3.2.2

只勾选web,即spring-boot-starter-web

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

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

java 复制代码
import java.lang.annotation.*;

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

3、编写一个判空注解

java 复制代码
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

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

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

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

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

实现就叫MyValidatorImpl

java 复制代码
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 (!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即可

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

java 复制代码
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、编写一个异常捕获,用于返回自定义的错误

java 复制代码
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、调用实现校验

编写一个接口:

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

http调用:http://localhost:8080/login

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

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

报错截图:

10、项目截图

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

gitee:gitee.com/lingkang_to...

github: github.com/xcocean/fin...

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

相关推荐
朝新_3 小时前
【多线程初阶】阻塞队列 & 生产者消费者模型
java·开发语言·javaee
立莹Sir3 小时前
Calendar类日期设置进位问题
java·开发语言
季鸢5 小时前
Java设计模式之状态模式详解
java·设计模式·状态模式
@yanyu6665 小时前
springboot实现查询学生
java·spring boot·后端
ascarl20105 小时前
准确--k8s cgroup问题排查
java·开发语言
magic 2455 小时前
Lombok 的 @Data 注解失效,未生成 getter/setter 方法引发的HTTP 406 错误
java
爱敲代码的憨仔5 小时前
分布式协同自动化办公系统-工作流引擎-流程设计
java·flowable·oa
纪元A梦6 小时前
分布式拜占庭容错算法——PBFT算法深度解析
java·分布式·算法
卿着飞翔6 小时前
RabbitMQ入门4.1.0版本(基于java、SpringBoot操作)
java·rabbitmq·java-rabbitmq
陈阿土i6 小时前
SpringAI 1.0.0 正式版——利用Redis存储会话(ChatMemory)
java·redis·ai·springai