springmvc的入参校验、hibernate-validator、spring-boot-starter-validation、final-validator、手撸validator
使用validation验证,spring中可以选择spring-boot-starter-validation
、hibernate-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-validation
、hibernate-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.Valid
和org.springframework.validation.annotation.Validated
也能开启校验。
基于此,如果我们开发一个 validation 校验框架,那么开启校验的注解必须是以Valid
开头的。
例如下面的
@PostMapping("/add")
public Object add(@Validated AddGoodsParam param){
// ...
return new ResponseResult<>().setData(param).setMsg("添加商品成功");
}
上面的@Validated
就是开启校验的注解了。
如何确定这个注解被校验?
回到上面的源码中,初始化数据绑定时
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
。这里我们自定义,不需要他们的花里胡哨功能:
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
http调用:http://localhost:8080/login?username=lk
http调用:http://localhost:8080/login?username=lk&password=lingkang
报错截图:
10、项目截图
至此,validator底层实现完成。我开发的 final-validator
框架就是基于此实现,更多细节你可以自行实现:
gitee:https://gitee.com/lingkang_top/final-validator
github: https://github.com/xcocean/final-validato
看到这里,希望帮我点点 start,感谢!