参考资料
👍👍👍0. Spring Validation最佳实践及其实现原理,参数校验没那么简单!
https://juejin.cn/post/6856541106626363399
✅0. Hibernate-Validator验证
https://www.yourbatman.cn/tags/Hibernate-Validator/
✅1. @Validated和@Valid的区别?教你使用它完成Controller参数校验(含级联属性校验)以及原理分析【享学Spring】
https://blog.csdn.net/f641385712/article/details/97621783
✅2. Bean Validation完结篇:你必须关注的边边角角(约束级联、自定义约束、自定义校验器、国际化失败消息…)【享学Spring】
https://blog.csdn.net/f641385712/article/details/97968775
✅3. 深入了解数据校验(Bean Validation):从深处去掌握@Valid的作用(级联校验)以及常用约束注解的解释说明【享学Java】
https://blog.csdn.net/f641385712/article/details/97042906
✅4. Spring方法级别数据校验:@Validated + MethodValidationPostProcessor优雅的完成数据校验动作【享学Spring】
https://blog.csdn.net/f641385712/article/details/97402946
✅5. 详述Spring对Bean Validation支持的核心API:Validator、SmartValidator、LocalValidatorFactoryBean…【享学Spring】
https://blog.csdn.net/f641385712/article/details/97270786
✅6. SpringBoot中BeanValidation数据校验与优雅处理详解
https://blog.csdn.net/Sky_QiaoBa_Sum/article/details/109767344
✅7. 在 Spring Boot 使用Bean Validation 完全指南
https://blog.csdn.net/Xiaowu_First/article/details/121444585
✅8. java自定义校验注解
https://dandelioncloud.cn/article/details/1497043194285232129
✅👍👍👍9. Springboot国际化i18n
https://www.dandelioncloud.cn/article/details/1525696496859365377
✅10. 手把手教你利用Spring Boot实现各种参数校验
https://www.jianshu.com/p/bffa168c29e8
✅11. 自定义容器类型元素验证,类级别验证(多字段联合验证)
https://blog.csdn.net/f641385712/article/details/109270066
✅12. 深入了解数据校验:Java Bean Validation 2.0(JSR303、JSR349、JSR380)Hibernate-Validation 6.x使用案例【享学Java】
https://blog.csdn.net/f641385712/article/details/96638596
✅13. 让Controller支持对平铺参数执行数据校验(默认Spring MVC使用@Valid
https://blog.csdn.net/f641385712/article/details/97621755
目录
一. 前期准备
1.1 ⏹自定义校验注解
⭕非空校验
import javax.validation.Constraint;
import javax.validation.constraints.NotEmpty;
import javax.validation.Payload;
import javax.validation.ReportAsSingleViolation;
import java.lang.annotation.*;
@Documented
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {})
@NotEmpty
@ReportAsSingleViolation
public @interface ValidateNotEmpty {
String msgArgs() default "";
// {1001E}所对应的错误消息存储在 XXX.properties 中
String message() default "{1001E}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
⭕日期格式校验
import org.springframework.util.ObjectUtils;
import javax.validation.*;
import java.lang.annotation.*;
import java.text.ParseException;
import java.text.SimpleDateFormat;
@Documented
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {ValidateDateString.StrictDateStringValidator.class})
public @interface ValidateDateString {
String msgArgs() default "";
String pattern() default "yyyy/MM/dd";
String message() default "{1004E}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
class StrictDateStringValidator implements ConstraintValidator<ValidateDateString, String> {
private String pattern;
@Override
public void initialize(ValidateDateString validStrictDateString) {
this.pattern = validStrictDateString.pattern();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
try {
if (ObjectUtils.isEmpty(value)) {
return true;
}
SimpleDateFormat sdf = new SimpleDateFormat(pattern);
// 设置非严格解析日期
sdf.setLenient(false);
sdf.parse(value);
} catch (ParseException e) {
return false;
}
return true;
}
}
}
⭕标记校验信息先后顺序的注解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
@ReportAsSingleViolation
public @interface CheckMsgOrder {
int value() default 0;
}
1.2 ⏹ValidationMessages.properties
校验信息
当Bean中的属性校验失败的时候,默认去
ValidationMessages.properties
文件中匹配错误信息.如果不想命名为ValidationMessages,需要额外的配置.此部分参照国际化校验错误消息的配置.
# 确认消息
1008Q=确定要放弃编辑的内容吗?
1019Q=确定要取消预约吗?
1021Q=确定要删除吗?
# 错误消息
1001E=请输入{msgArgs}。
1002E=请选择{msgArgs}。
1003E=请输入{msgArgs}全角假名。
1004E=输入的{msgArgs}日期格式不正确。
1005E=请输入半角数字。
1006E={msgArgs}最多不能超过{max}文字。
1.3 ⏹封装校验错误信息的实体类
被
@JsonIgnore
注解标记的属性,不会携带值返回到前台
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
@Data
public class ErrorItemEntity implements Serializable {
// 错误信息ID
@JsonIgnore
private String errorMessageId;
// 错误信息
private String errorMessage;
// 前台错误项目ID
private String errorItemId;
// 前台输入的值
@JsonIgnore
private Object rejectValue;
// 错误的行号
@JsonIgnore
private String errorRowNo;
// 错误的ID列表
private List<String> errorIdList;
public static ErrorItemEntity of(String message) {
return ErrorItemEntity.of(message, "", "");
}
public static ErrorItemEntity of(String message, String errorItemId) {
return ErrorItemEntity.of(message, errorItemId, "");
}
public static ErrorItemEntity of(String errorMessage, String errorItemId, Object rejectValue) {
ErrorItemEntity entity = new ErrorItemEntity();
entity.errorMessage = errorMessage;
entity.errorItemId = errorItemId;
entity.rejectValue = rejectValue;
return entity;
}
public static ErrorItemEntity of(String errorMessage, String errorRowNo, List<String> errorIdList) {
ErrorItemEntity entity = new ErrorItemEntity();
entity.errorMessage = errorMessage;
entity.errorRowNo = errorRowNo;
entity.errorIdList = errorIdList;
return entity;
}
}
1.4 ⏹返回结果封装类
前台Ajax请求后台时,统一使用下面的实体类返回数据到前台
@Data
@EqualsAndHashCode(callSuper = false)
public class ResultEntity implements Serializable {
private boolean result;
private List<ErrorItemEntity> errors = new ArrayList<>();
private Object entity;
public ResultEntity() {
}
/**
* 构造函数
*
* @param result 处理结果,true或者false
* @param errors 错误信息
* @param entity 返回给前台的信息
*/
private ResultEntity(boolean result, List<ErrorItemEntity> errors, Object entity) {
this.result = result;
this.errors = (errors == null) ? new ArrayList<>() : errors;
this.entity = entity;
}
/**
* 请求成功,并且返回值给前台
*
* @param entity 返回给前台的信息
*/
public static ResultEntity ok(Object entity) {
return new ResultEntity(true, null, entity);
}
/**
* 请求成功,不返回值给前台
*/
public static ResultEntity ok() {
return new ResultEntity(true, null, null);
}
/**
* 请求失败
*/
public static ResultEntity ng() {
return new ResultEntity(false, null, null);
}
}
1.5 ⏹自定义校验异常
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class ValidationException extends RuntimeException {
// 错误信息
private List<ErrorItemEntity> errors;
/**
* 生成ValidationException异常对象
*
* @param bindingResult 注解校验结果
*/
public ValidationException(BindingResult bindingResult) {
super();
this.errors = ErrorInfoUtils.getErrorResult(bindingResult);
}
/**
* 生成ValidationException异常对象
*
* @param errors 业务异常信息
*/
public ValidationException(List<ErrorItemEntity> errors) {
super();
this.errors = errors;
}
}
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import java.util.ArrayList;
import java.util.List;
public final class ErrorInfoUtils {
private ErrorInfoUtils() { }
/**
* 从注解校验结果中获取错误信息
*
* @param bindingResult 注解校验结果
* @return 错误信息
*/
public static List<ErrorItemEntity> getErrorResult(BindingResult bindingResult) {
List<ErrorItemEntity> errors = new ArrayList<>();
for (FieldError fieldError : bindingResult.getFieldErrors()) {
errors.add(ErrorItemEntity.of(fieldError.getDefaultMessage(), fieldError.getField(), fieldError.getRejectedValue()));
}
return errors;
}
}
1.6 ⏹全局异常捕获
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.RedirectView;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
@ControllerAdvice
public class GlobalExceptionHandler {
@Autowired
private HttpServletRequest request;
@Autowired
private HttpServletResponse response;
/*
当Controller层使用下面这种写法校验数据时
commonValidate2(@RequestBody @Validated Test4Form form)
由于没有使用BindingResult接收注解的check结果,
此时会抛出MethodArgumentNotValidException异常,
我们在全局异常捕获类,对此异常尽心处理
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ModelAndView HandleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
BindingResult bindingResult = ex.getBindingResult();
List<ErrorItemEntity> errorResult = ErrorInfoUtils.getErrorResult(bindingResult);
// 校验失败,返回400的状态码
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
// 自定义结果封装类
ResultEntity resultEntity = ResultEntity.ng();
resultEntity.setErrors(errorResult);
return this.commonErrorHandle(resultEntity);
}
// 捕获我们收到抛出的ValidationException异常,进行处理
@ExceptionHandler(ValidationException.class)
// 通过注解的方式指定相应状态码
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ModelAndView HandleValidationException(ValidationException ex) {
// 自定义结果封装类
ResultEntity resultEntity = ResultEntity.ng();
if (ex.getErrors() != null) {
resultEntity.setErrors(ex.getErrors());
}
return this.commonErrorHandle(resultEntity);
}
private ModelAndView commonErrorHandle(ResultEntity resultEntity) {
// 如果是Ajax请求的话
if ("XMLHttpRequest".equals(request.getHeader("X-Requested-With"))) {
// MappingJackson2JsonView的作用是把model转换为json
MappingJackson2JsonView jsonView = new MappingJackson2JsonView();
jsonView.setExtractValueFromSingleKeyModel(true);
// ModelAndView是通过MappingJackson2JsonView构成的,因此并不定位到视图,而是返回json数据
ModelAndView mv = new ModelAndView(jsonView);
mv.addObject("jsonKey", resultEntity);
return mv;
}
// 重定向到错误页面
return new ModelAndView(new RedirectView(request.getContextPath() + "/systemError"));
}
}
1.7 ⏹待校验的Bean
import lombok.Data;
import javax.validation.Valid;
import javax.validation.groups.Default;
import java.util.List;
@Data
public class Test4Form {
// 用来表示错误信息的先后顺序
@CheckMsgOrder(value = 1)
@ValidateNotEmpty(msgArgs = "from日期", groups = {Default.class})
@ValidateDateString(msgArgs = "from日期", pattern = "yyyyMMdd", groups = {Default.class})
private String fromData;
@CheckMsgOrder(value = 2)
@ValidateNotEmpty(msgArgs = "to日期", groups = {Default.class})
@ValidateDateString(msgArgs = "to日期", pattern = "yyyyMMdd", groups = {Default.class})
private String toData;
/*
在指定校验AgeGroup分组时,只有被标记了AgeGroup.class的注解才会其作用
如果指定校验Default.class,所有的注解都会其作用
*/
@CheckMsgOrder(value = 2)
@ValidateHalfNumeric(groups = {AgeGroup.class, Default.class})
private String age;
// 定义一个age分组,
public interface AgeGroup { }
}
二. 校验
2.1 普通的校验
⭕前台
$("#btn1").click(() => {
operateFlag = !operateFlag;
const url = `http://localhost:8080/test4/commonValidate1`;
const validateParam = {
fromData: '2021pp',
age: 'asc',
};
doAjax(url, validateParam, function(data) {
console.log(data);
});
});
⭕后台
使用
@Validated
注解校验Bean中自定义注解,对属性进行校验.
@PostMapping("/commonValidate1")
@ResponseBody
public ResultEntity commonValidate1(@RequestBody @Validated Test4Form form, BindingResult bindingResult) {
// 使用BindingResult接收注解的check结果,手动抛出自定义异常
if (bindingResult.hasErrors()) {
throw new ValidationException(bindingResult);
}
System.out.println(form);
return ResultEntity.ok();
}
⭕效果
2.2 不使用BindingResult接收注解的校验结果
⭕后台
1.不使用BindingResult接收注解的check结果,会抛出
MethodArgumentNotValidException
异常.需要在全局异常捕获类,处理此异常.
2.因为我们在GlobalExceptionHandler
的HandleMethodArgumentNotValidException方法中对异常进行了捕获,因此前台也能正确获取到后台响应.
@PostMapping("/commonValidate2")
@ResponseBody
public ResultEntity commonValidate2(@RequestBody @Validated Test4Form form) {
System.out.println(form);
return ResultEntity.ok();
}
⭕效果
2.3 分组check
⭕前台
$("#btn2").click(() => {
const url = "http://localhost:8080/test4/ageGroupValidate";
const validateParam = {
fromData: '2021pp',
// age: 'asc',
};
doAjax(url, validateParam, function(data) {
console.log(data);
});
});
⭕后台
使用
@Validated(Test4Form.AgeGroup.class)
注解,声明只校验标记了AgeGroup分组的属性.未标记AgeGroup分组的属性,一概不校验.
@PostMapping("/ageGroupValidate")
@ResponseBody
public ResultEntity ageGroupValidate(@RequestBody @Validated(Test4Form.AgeGroup.class) Test4Form form,
BindingResult validateResult) {
if (validateResult.hasErrors()) {
throw new ValidationException(validateResult);
}
System.out.println(form);
return ResultEntity.ok();
}
⭕效果
2.4 LocalValidatorFactoryBean手动校验
1.除了使用
@Validated
在Controller层进行校验之外,还可以使用LocalValidatorFactoryBean
进行校验,同样也支持分组.
2.默认情况下,校验之后的error消息是无序的,即属性1到属性5的都被校验住之后,属性5所对应的错误消息可能出现在属性1之前,此时可通过自定义注解@CheckMsgOrder
来对错误消息排序.
⭕前台
let operateFlag = false;
$("#btn3").click(() => {
operateFlag = !operateFlag;
const url = "http://localhost:8080/test4/conditionGroupValidate";
const validateParam = {
fromData: '2021pp',
age: 'asc',
operateFlag,
};
doAjax(url, validateParam, function(data) {
console.log(data);
});
});
⭕后台
@Autowired
private Test4Service service;
@PostMapping("/conditionGroupValidate")
@ResponseBody
public ResultEntity conditionGroupValidate(@RequestBody Test4Form form) {
// 在业务层对Bean进行校验
service.check(form);
System.out.println(form);
return ResultEntity.ok();
}
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import javax.validation.ConstraintViolation;
import javax.validation.groups.Default;
@Service
public class Test4Service {
// 注入校验工厂类
@Autowired
private LocalValidatorFactoryBean validator;
public void check(Test4Form form) {
// 使用JDK的校验对象(无法check一览中的值,只有Spring提供的LocalValidatorFactoryBean才能都check)
// Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Set<ConstraintViolation<Test4Form>> errorResult;
// 记性条件进行分组check,校验全部或者仅校验AgeGroup分组
if (!form.getOperateFlag()) {
errorResult = validator.validate(form, Default.class);
} else {
errorResult = validator.validate(form, Test4Form.AgeGroup.class);
}
// 若存在错误消息的话
if (!ObjectUtils.isEmpty(errorResult) && errorResult.size() > 0) {
Map<Integer, ErrorItemEntity> itemEntityHashMap = new HashMap<>();
List<ErrorItemEntity> tableItemList = new ArrayList<>();
/*
默认校验之后的errorResult是无序的,我们通过其中的getPropertyPath
获取到bean中的属性名称,然后通过反射工具类,获取该属性上标记的
@CheckMsgOrder注解所对应的value值,然后根据此value值进行排序
*/
for (ConstraintViolation<Test4Form> error : errorResult) {
String errorFieldName = error.getPropertyPath().toString();
int checkOrderValue = ReflectionUtil.getCheckOrderValue(form.getClass(), errorFieldName);
if (checkOrderValue == 0) {
tableItemList.add(ErrorItemEntity.of(error.getMessage(), errorFieldName));
continue;
}
itemEntityHashMap.put(checkOrderValue, ErrorItemEntity.of(error.getMessage(), errorFieldName));
}
// 根据Map中的key进行排序,保证错误消息按照注解@CheckMsgOrder的值由小到大进行排序
List<ErrorItemEntity> errorList = new ArrayList<>();
itemEntityHashMap.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.forEachOrdered(item -> errorList.add(item.getValue()));
errorList.addAll(tableItemList);
throw new ValidationException(errorList);
}
}
}
⭕效果
- 当operateFlag为true
- 当operateFlag为false
2.5 对Bean中的实体类List进行校验
⭕前台
const tableList = [
{
id: null,
address: '测试address123',
hobby: '测试hobby123'
},
{
id: 110,
address: '测试',
hobby: '测试AAAAAAAAAA'
},
{
id: 120
}
];
$("#btn4").click(() => {
const url = 'http://localhost:8080/test4/validateAnnotationTableList';
const validateParam = {
fromData: '2021pp',
age: 'asc',
tableList,
};
doAjax(url, validateParam, function(data) {
console.log(data);
});
});
⭕后台
嵌套校验的情况下,需要使用
@Valid
注解
向Test4Form中追加属性
import javax.validation.Valid;
@Data
public class Test4Form {
// ...
@Valid
private List<Test4Entity> tableList;
// ...
}
@Data
public class Test4Entity {
@ValidateNotEmpty(msgArgs = "ID项目", groups = {Default.class})
private String id;
@ValidateSize(msgArgs = "地址项目", max = 6, groups = {Default.class})
private String address;
@ValidateSize(msgArgs = "兴趣项目", max = 5, groups = {Default.class})
private String hobby;
}
@PostMapping("/validateAnnotationTableList")
@ResponseBody
public ResultEntity validateAnnotationTableList(@RequestBody @Validated(Default.class) Test4Form form) {
System.out.println(form);
return ResultEntity.ok();
}
⭕效果
三. 国际化校验
⭕前台
let operateFlag = false;
$("#btn4").click(() => {
operateFlag = !operateFlag;
// 根据operateFlag来决定,返回的校验消息是中文还是日文
const url = `http://localhost:8080/test4/validateAnnotationTableList?language=${operateFlag ? 'zh' : 'jp'}`;
const validateParam = {
fromData: '2021pp',
age: 'asc',
tableList,
operateFlag,
};
doAjax(url, validateParam, function(data) {
console.log(data);
});
});
⭕后台配置
根据前台传入的?language所对应的值来指定要返回的国家语言错误消息
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
import java.util.Locale;
@Configuration
public class InternationalConfig implements WebMvcConfigurer {
// 默认解析器,用来设置当前会话默认的国际化语言
@Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver sessionLocaleResolver = new SessionLocaleResolver();
// 指定当前项目的默认语言是中文
sessionLocaleResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
return sessionLocaleResolver;
}
// 默认拦截器,用来指定切换国际化语言的参数名
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
/*
设置国际化请求参数为language
设置完成之后,URL中的 ?language=zh 表示读取国际化文件messages_zh.properties
*/
localeChangeInterceptor.setParamName("language");
return localeChangeInterceptor;
}
// 自定义国际化环境下要显示的校验消息
@Bean
public LocalValidatorFactoryBean localValidatorFactoryBean() {
LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean();
// 使用Spring加载国际化资源文件
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
/*
设置资源文件的前缀名称
配合 localeChangeInterceptor 中设置的?language所对应的参数值
就可以加载对应国际化校验消息
*/
messageSource.setBasename("messages");
messageSource.setDefaultEncoding("UTF-8");
localValidatorFactoryBean.setValidationMessageSource(messageSource);
return localValidatorFactoryBean;
}
// 将我们自定义的国际化语言参数拦截器放入Spring MVC的默认配置中
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
}
⭕国际化校验文件
messages_jp.properties
# 情報I
1031I=予約枠の一括作成を行いました。
1032I=予約枠の一括削除を行いました。
1033I=予約枠状態の一括変更を行いました。
# エラーE
1001E={msgArgs}を入力してください。
1002E={msgArgs}を選択してください。
1003E={msgArgs}は全角カタカナまたは半角アルファベットで入力してください。
1004E=入力された{msgArgs}日付は妥当ではありません。
1005E=半角数字を入力してください。
1006E={msgArgs}を{max}文字以内で入力してください。
messages_zh.properties
# 確認Q
1008Q=确定要放弃编辑的内容吗?
1019Q=确定要取消预约吗?
1021Q=确定要删除吗?
# エラーE
1001E=请输入{msgArgs}。
1002E=请选择{msgArgs}。
1003E=请输入{msgArgs}全角假名。
1004E=输入的{msgArgs}日期格式不正确。
1005E=请输入半角数字。
1006E={msgArgs}最多不能超过{max}文字。
⭕效果
- ?language=zh
- ?language=jp