Я уже писал оSpring Validation
Статья б/у, но самоощущение пока поверхностное, в этот раз планирую полностью разобратьсяSpring Validation
. В этой статье будет подробно рассказаноSpring Validation
Лучшие практики и принципы их реализации в различных сценариях, умирайте до конца!
Исходный код проекта:spring-validation
Простой в использовании
Java API
Спецификация(JSR303
)ОпределенныйBean
эталон калибровкиvalidation-api
, но реализация не предусмотрена.hibernate validation
является реализацией этой спецификации и добавляет аннотации проверки, такие как@Email
,@Length
Ждать.Spring Validation
правдаhibernate validation
дополнительный пакет для поддержкиspring mvc
Параметры проверяются автоматически. Далее мы используемspring-boot
Проект как пример, введениеSpring Validation
использование.
импортировать зависимости
еслиspring-boot
версия меньше чем2.3.x
,spring-boot-starter-web
автоматически войдетhibernate-validator
полагаться. еслиspring-boot
версия больше, чем2.3.x
, вам нужно вручную ввести зависимости:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.1.Final</version>
</dependency>
заweb
Для услуг, чтобы предотвратить влияние незаконных параметров на бизнес,Controller
Слой должен выполнить проверку параметров! В большинстве случаев параметры запроса делятся на следующие две формы:
-
POST
,PUT
запросить, использоватьrequestBody
параметры передачи; -
GET
запросить, использоватьrequestParam/PathVariable
Передать параметры.
Ниже мы кратко представимrequestBody
иrequestParam/PathVariable
Параметр проверки реального боя!
requestBody
проверка параметров
POST
,PUT
запрос обычно используетrequestBody
Передайте параметры, в этом случае бэкэнд используетобъект DTOполучить.Просто добавьте объект DTO@Validated
Аннотация может реализовать автоматическую проверку параметров. Например, есть сохранениеUser
интерфейс, требуетсяuserName
длина2-10
,account
иpassword
длина поля6-20
. Если проверка не удалась, выброситьMethodArgumentNotValidException
аномальный,Spring
По умолчанию он будет преобразован в400(Bad Request)
просить.
DTO представляет собой объект передачи данных (Data Transfer Object), который используется для интерактивной передачи между сервером и клиентом.. В проекте spring-web можно представить метод, используемый для получения параметров запроса
Bean
объект.
- существует
DTO
Объявите аннотации ограничений для полей
@Data
public class UserDTO {
private Long userId;
@NotNull
@Length(min = 2, max = 10)
private String userName;
@NotNull
@Length(min = 6, max = 20)
private String account;
@NotNull
@Length(min = 6, max = 20)
private String password;
}
- Объявить аннотации проверки для параметров метода
@PostMapping("/save")
public Result saveUser(@RequestBody @Validated UserDTO userDTO) {
// 校验通过,才会执行业务逻辑处理
return Result.ok();
}
В этой ситуации,использовать
@Valid
и@Validated
все будет хорошо.
requestParam/PathVariable
проверка параметров
GET
запрос обычно используетrequestParam/PathVariable
Передать параметры. Если параметров много (например, более 6), рекомендуется использоватьDTO
Объект получает. В противном случае рекомендуется размещать параметры один за другим в параметрах метода. при этих обстоятельствах,Должен бытьController
ярлык на классе@Validated
аннотации и объявить аннотации ограничений для входных параметров (например,@Min
Ждать). Если проверка не удалась, выброситьConstraintViolationException
аномальный. Пример кода выглядит следующим образом:
@RequestMapping("/api/user")
@RestController
@Validated
public class UserController {
// 路径变量
@GetMapping("{userId}")
public Result detail(@PathVariable("userId") @Min(10000000000000000L) Long userId) {
// 校验通过,才会执行业务逻辑处理
UserDTO userDTO = new UserDTO();
userDTO.setUserId(userId);
userDTO.setAccount("11111111111111111");
userDTO.setUserName("xixi");
userDTO.setAccount("11111111111111111");
return Result.ok(userDTO);
}
// 查询参数
@GetMapping("getByAccount")
public Result getByAccount(@Length(min = 6, max = 20) @NotNull String account) {
// 校验通过,才会执行业务逻辑处理
UserDTO userDTO = new UserDTO();
userDTO.setUserId(10000000000000003L);
userDTO.setAccount(account);
userDTO.setUserName("xixi");
userDTO.setAccount("11111111111111111");
return Result.ok(userDTO);
}
}
Унифицированная обработка исключений
Как упоминалось ранее, если проверка не пройдена,MethodArgumentNotValidException
илиConstraintViolationException
аномальный. В реальной разработке проекта обычно используетсяУнифицированная обработка исключенийчтобы вернуть более дружественную подсказку. Например, наша система требует, чтобы независимо от того, какое исключение было отправлено,http
Код состояния должен быть возвращен200
служебный код используется для определения нештатной ситуации в системе.
@RestControllerAdvice
public class CommonExceptionHandler {
@ExceptionHandler({MethodArgumentNotValidException.class})
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
BindingResult bindingResult = ex.getBindingResult();
StringBuilder sb = new StringBuilder("校验失败:");
for (FieldError fieldError : bindingResult.getFieldErrors()) {
sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");
}
String msg = sb.toString();
return Result.fail(BusinessCode.参数校验失败, msg);
}
@ExceptionHandler({ConstraintViolationException.class})
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public Result handleConstraintViolationException(ConstraintViolationException ex) {
return Result.fail(BusinessCode.参数校验失败, ex.getMessage());
}
}
Расширенное использование
Проверка пакетов
В реальных проектах может потребоваться несколько методов для использования одного и того жеDTO
класс для получения параметров, и правила проверки разных методов, вероятно, будут разными. В это время простоDTO
Добавление аннотаций ограничений к полям класса не решает эту проблему. следовательно,spring-validation
поддерживаетсяПроверка пакетовФункция предназначена для решения такого рода проблем. Или приведенный выше пример, такой как сохранениеUser
когда,UserId
обнуляемый, но обновляетUser
когда,UserId
Значение должно>=10000000000000000L
; Правила проверки для других полей одинаковы в обоих случаях. использовать это времяПроверка пакетовПример кода выглядит следующим образом:
- Применимая информация о группировке объявляется в аннотации ограничения.
groups
@Data
public class UserDTO {
@Min(value = 10000000000000000L, groups = Update.class)
private Long userId;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 2, max = 10, groups = {Save.class, Update.class})
private String userName;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 6, max = 20, groups = {Save.class, Update.class})
private String account;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 6, max = 20, groups = {Save.class, Update.class})
private String password;
/**
* 保存的时候校验分组
*/
public interface Save {
}
/**
* 更新的时候校验分组
*/
public interface Update {
}
}
@Validated
Укажите контрольную группу на аннотации
@PostMapping("/save")
public Result saveUser(@RequestBody @Validated(UserDTO.Save.class) UserDTO userDTO) {
// 校验通过,才会执行业务逻辑处理
return Result.ok();
}
@PostMapping("/update")
public Result updateUser(@RequestBody @Validated(UserDTO.Update.class) UserDTO userDTO) {
// 校验通过,才会执行业务逻辑处理
return Result.ok();
}
Вложенные проверки
В предыдущем примереDTO
Поля в классе基本数据类型
иString
тип. Но в реальной сцене возможно, что поле также является объектом, в этом случае вы можете использовать嵌套校验
. Например, сохраните вышеUser
информация вместе сJob
Информация. должны знать о том,В настоящее времяDTO
Соответствующее поле класса должно быть отмечено@Valid
аннотация.
@Data
public class UserDTO {
@Min(value = 10000000000000000L, groups = Update.class)
private Long userId;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 2, max = 10, groups = {Save.class, Update.class})
private String userName;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 6, max = 20, groups = {Save.class, Update.class})
private String account;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 6, max = 20, groups = {Save.class, Update.class})
private String password;
@NotNull(groups = {Save.class, Update.class})
@Valid
private Job job;
@Data
public static class Job {
@Min(value = 1, groups = Update.class)
private Long jobId;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 2, max = 10, groups = {Save.class, Update.class})
private String jobName;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 2, max = 10, groups = {Save.class, Update.class})
private String position;
}
/**
* 保存的时候校验分组
*/
public interface Save {
}
/**
* 更新的时候校验分组
*/
public interface Update {
}
}
Вложенные проверки можно использовать в сочетании с групповыми проверками. И есть
嵌套集合校验
Каждый элемент в коллекции будет проверен, напримерList<Job>
поле будетlist
каждый внутриJob
Объекты проверены.
установить чек
Если тело запроса передается напрямуюjson
Массив отдается фону, и есть надежда, что проверка параметров выполняется для каждого элемента в массиве. На данный момент, если мы напрямую используемjava.util.Collection
внизlist
илиset
для получения данных проверка параметров не вступит в силу! Мы можем использовать пользовательскиеlist
Сбор для получения параметров:
- Упаковка
List
введите и объявите@Valid
аннотация
public class ValidationList<E> implements List<E> {
@Delegate // @Delegate是lombok注解
@Valid // 一定要加@Valid注解
public List<E> list = new ArrayList<>();
// 一定要记得重写toString方法
@Override
public String toString() {
return list.toString();
}
}
@Delegate
Аннотации подлежатlombok
ограничения версии,1.18.6
Вышеупомянутые версии поддерживаются. Если проверка не удалась, он выдастNotReadablePropertyException
, которые также можно обрабатывать с помощью унифицированных исключений.
Например, нам нужно сохранить несколькоUser
объект,Controller
Метод слоя можно записать так:
@PostMapping("/saveList")
public Result saveList(@RequestBody @Validated(UserDTO.Save.class) ValidationList<UserDTO> userList) {
// 校验通过,才会执行业务逻辑处理
return Result.ok();
}
таможенный чек
Бизнес-требования всегда сложнее, чем простые проверки, предоставляемые платформой, и мы можем настроить проверки в соответствии со своими потребностями. настроитьspring validation
Очень просто, если мы настраиваем加密id
(по номерам илиa-f
состоит из букв,32-256
длина) проверки, которая в основном делится на два этапа:
- Аннотации пользовательских ограничений
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {EncryptIdValidator.class})
public @interface EncryptId {
// 默认错误消息
String message() default "加密id格式错误";
// 分组
Class<?>[] groups() default {};
// 负载
Class<? extends Payload>[] payload() default {};
}
- выполнить
ConstraintValidator
Валидатор ограничения записи интерфейса
public class EncryptIdValidator implements ConstraintValidator<EncryptId, String> {
private static final Pattern PATTERN = Pattern.compile("^[a-f\\d]{32,256}$");
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 不为null才进行校验
if (value != null) {
Matcher matcher = PATTERN.matcher(value);
return matcher.find();
}
return true;
}
}
Таким образом, мы можем использовать@EncryptId
Проверка параметров выполнена!
Программная проверка
Приведенные выше примеры основаны на注解
Чтобы добиться автоматической проверки, в некоторых случаях мы можем захотеть использовать编程方式
Проверка звонка. можно вводить в это времяjavax.validation.Validator
объект, а затем вызвать егоapi
.
@Autowired
private javax.validation.Validator globalValidator;
// 编程式校验
@PostMapping("/saveWithCodingValidate")
public Result saveWithCodingValidate(@RequestBody UserDTO userDTO) {
Set<ConstraintViolation<UserDTO>> validate = globalValidator.validate(userDTO, UserDTO.Save.class);
// 如果校验通过,validate为空;否则,validate包含未校验通过项
if (validate.isEmpty()) {
// 校验通过,才会执行业务逻辑处理
} else {
for (ConstraintViolation<UserDTO> userDTOConstraintViolation : validate) {
// 校验失败,做其它逻辑
System.out.println(userDTOConstraintViolation);
}
}
return Result.ok();
}
Быстрая ошибка
Spring Validation
По умолчанию перед созданием исключения проверяются все поля. Его можно включить с помощью некоторой простой конфигурацииFali Fast
режим и немедленно возвращается, если проверка не удалась.
@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
// 快速失败模式
.failFast(true)
.buildValidatorFactory();
return validatorFactory.getValidator();
}
@Valid
и@Validated
разница
разница | @Valid | @Validated |
---|---|---|
провайдер | Спецификация JSR-303 | Spring |
Поддерживать ли группировку | не поддерживается | служба поддержки |
Местоположение ярлыка | METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE | TYPE, METHOD, PARAMETER |
Вложенные проверки | служба поддержки | не поддерживается |
Принцип реализации
requestBody
Принцип проверки параметров
существуетspring-mvc
середина,RequestResponseBodyMethodProcessor
для разбора@RequestBody
Аннотированные параметры и обработка@ResponseBody
Аннотировать возвращаемое значение метода. Очевидно, что логика выполнения проверки параметров должна быть в методе разбора параметров.resolveArgument()
середина:
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
parameter = parameter.nestedIfOptional();
//将请求数据封装到DTO对象中
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
String name = Conventions.getVariableNameForParameter(parameter);
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
if (arg != null) {
// 执行数据校验
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
if (mavContainer != null) {
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
}
return adaptArgumentIfNecessary(arg, parameter);
}
}
можно увидеть,resolveArgument()
называетсяvalidateIfApplicable()
Выполните проверку параметров.
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
// 获取参数注解,比如@RequestBody、@Valid、@Validated
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation ann : annotations) {
// 先尝试获取@Validated注解
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
//如果直接标注了@Validated,那么直接开启校验。
//如果没有,那么判断参数前是否有Valid起头的注解。
if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
//执行校验
binder.validate(validationHints);
break;
}
}
}
Увидев это, каждый должен понять, почему в этом сценарии@Validated
,@Valid
Две аннотации можно смешивать. продолжим смотретьWebDataBinder.validate()
выполнить.
@Override
public void validate(Object target, Errors errors, Object... validationHints) {
if (this.targetValidator != null) {
processConstraintViolations(
//此处调用Hibernate Validator执行真正的校验
this.targetValidator.validate(target, asValidationGroups(validationHints)), errors);
}
}
Наконец обнаружил, что нижний слой, наконец, называетсяHibernate Validator
Выполните настоящую проверку.
Принцип реализации проверки параметров на уровне метода
Как упоминалось выше, параметры встраиваются в параметры метода один за другим, а затем объявляются перед каждым параметром.约束注解
Метод проверки — это проверка параметра на уровне метода. На самом деле, этот метод можно использовать для любогоSpring Bean
метод, такой какController
/Service
Ждать.В основе лежит принцип реализацииAOP
, в частности,MethodValidationPostProcessor
динамическая регистрацияAOP
нарезать, а затем использоватьMethodValidationInterceptor
Улучшения плетения для методов pointcut.
public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessorimplements InitializingBean {
@Override
public void afterPropertiesSet() {
//为所有`@Validated`标注的Bean创建切面
Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
//创建Advisor进行增强
this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
}
//创建Advice,本质就是一个方法拦截器
protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
}
}
Тогда взглянитеMethodValidationInterceptor
:
public class MethodValidationInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
//无需增强的方法,直接跳过
if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
return invocation.proceed();
}
//获取分组信息
Class<?>[] groups = determineValidationGroups(invocation);
ExecutableValidator execVal = this.validator.forExecutables();
Method methodToValidate = invocation.getMethod();
Set<ConstraintViolation<Object>> result;
try {
//方法入参校验,最终还是委托给Hibernate Validator来校验
result = execVal.validateParameters(
invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
}
catch (IllegalArgumentException ex) {
...
}
//有异常直接抛出
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
//真正的方法调用
Object returnValue = invocation.proceed();
//对返回值做校验,最终还是委托给Hibernate Validator来校验
result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
//有异常直接抛出
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
return returnValue;
}
}
В самом деле, будь тоrequestBody参数校验
все еще方法级别的校验
, в конце концов звонитHibernate Validator
выполнить проверку,Spring Validation
Просто сделайте слой упаковки.
Нелегко быть оригинальным. Если вы считаете, что статья написана хорошо, пожалуйста, поставьте лайк 👍 и поддержите ее~
Добро пожаловать в мой проект с открытым исходным кодом:Облегченная среда вызовов HTTP для SpringBoot