团队新来了个校招实习生静静,相互交流后发现竟然是我母校同实验室的小学妹,小学妹很热情地认下了我这个失散多年的大湿哥,后来...
小学妹:大湿哥,咱们项目里的 Controller 怎么都看不到参数校验处理的代码呀?但是程序运行起来,看到有是有校验的?
大湿哥:哦哦,静静,你看到 Controller 类和方法上的 @Validated,还有其他参数的 @NotBlank、@Size 这些注解了吗?
小学妹:看到了,你的意思是这些注解跟参数校验的处理有关系?
大湿哥:对呀!是不是觉得咱们项目上 Controller 的代码特清爽。
小学妹:嗯嗯,很干净,完全没有我在学校写的项目那一大坨校验的代码。大湿哥能给我讲讲是怎么一回事吗?
大湿哥:好吧!这里是利用了 Bean Validation 的技巧,下面我来详细讲讲。

API 是每个 Web 项目中必不可少的部分,后端开发人员除了要处理大量的 CRUD 逻辑之外,接口的参数校验与响应格式的规范处理也都占用了大量的精力。
在接下来的几篇文章中,我们将介绍 API 编写的实战技巧,让从请求到响应的接口编写更加优雅、高效。
这篇我们来讨论接口请求参数校验。
接口常规校验案例
我们来定义一个用户对象 - UserDTO,包含用户名、密码、性别及地址。地址对象 - AddressDTO,包含省份、城市、详细地址。
用户对象的字段有如下约束:
- 用户名:
- 用户账号不能为空
- 账号长度必须是6-11个字符
- 密码:
- 密码长度必须是6-16个字符
- 性别:
- 性别只能为 0:未知,1:男,2:女
- 地址:
- 地址信息不能为空
UserDTO
public class UserDTO { /** * 校验规则: * * 1. 用户账号不能为空 * 2. 账号长度必须是6-11个字符 */ private String name; /** * 校验规则: * 1. 密码长度必须是6-16个字符 */ private String password; /** * 校验规则: * * 1. 性别只能为 0:未知,1:男,2:女" */ private int sex; /** * 校验规则: * * 1. 地址信息不能为空 */ private AddressDTO address; // 省略 Getter/Setter} AddressDTO
public class AddressDTO { private String province; private String city; private String detail; // 省略 Getter/Setter} UserController
@RestController@RequestMapping("/users")public class UserController { @PostMapping("/create") public UserDTO create(@RequestBody UserDTO userDTO) { if (userDTO == null) { // 此为示例代码,正式项目中一般不使用 System.out.println 打印日志 System.out.println("用户信息不能为空"); return null; } // 校验用户账户 String name = userDTO.getName(); if (name == null || name.trim() == "") { System.out.println("用户账号不能为空"); return null; } else { int len = name.trim().length(); if (len < 6 || len > 11) { System.out.println("密码长度必须是6-11个字符"); return null; } } // 校验密码,抽出一个方法,与校验用户账户的代码做比较 if (validatePassword(userDTO) == null) { return null; } // 校验性别 int sex = userDTO.getSex(); if (sex < 0 || sex > 2) { System.out.println("性别只能为 0:未知,1:男,2:女"); return null; } // 校验地址 validateAddress(userDTO.getAddress()); // 校验完成后,请求的用户信息有效,开始处理用户插入等逻辑,操作成功以后响应。 return userDTO; } // 校验地址,通过抛出异常的方式来处理 private void validateAddress(AddressDTO addressDTO) { if (addressDTO == null) { // 也可以通过抛出异常来处理 throw new RuntimeException("地址信息不能为空"); } validateAddressField(addressDTO.getProvince(), "所在省份不能为空"); validateAddressField(addressDTO.getCity(), "所在城市不能为空"); validateAddressField(addressDTO.getDetail(), "详细地址不能为空"); } // 校验地址中的每个字段,并返回对应的信息 private void validateAddressField(String field, String msg) { if (field == null || field.equals("")) { throw new RuntimeException(msg); } } // 将校验密码的操作抽取到一个方法中 private UserDTO validatePassword(@RequestBody UserDTO userDTO) { String password = userDTO.getPassword(); if (password == null || password.trim() == "") { System.out.println("用户密码不能为空"); return null; } else { int len = password.trim().length(); if (len < 6 || len > 16) { System.out.println("账号长度必须是6-16个字符"); return null; } } return userDTO; }}在 UserController 中,我们定义了创建用户的接口 /users/create。在正式开始业务逻辑处理之前,为了保证接收到的参数有效,我们根据规则编写了大量的校验代码,即使我们可以采取抽取方法等重构手段进行复用,但依然需要对校验规则劳心劳力。
那有没有什么技巧,能够避免编写这大量的参数校验代码呢?
Bean Validation 规范与其实现
上面问题的答案当然是:有!
实际上,Java 早在 2009 年就提出了 Bean Validation 规范,该规范定义的是一个运行时的数据验证框架,在验证之后验证的错误信息会被马上返回。并且已经历经 JSR303、JSR349、JSR380 三次标准的制定,发展到了 2.0。
JSR 规范提案只是提供了规范,并没有提供具体的实现。具体实现框架有默认的 javax.validation.api,以及 hibernate-validator。目前绝大多使用 hibernate-validator。
javax.validation.api
Java 在 2009 年的 JAVAEE 6 中发布了 JSR303 以及 javax 下的 validation 包内容。这项工作的主要目标是为 java 应用程序开发人员提供 基于 java 对象的 约束(constraints)声明和对约束的验证工具(validator),以及约束元数据存储库和查询 API,以及默认实现。
Java8 开始,Java EE 改名为 Jakarta EE,注意 javax.validation 相关的包移动到了 jakarta.validation 的包下。所以大家看不同的版本的时候,会发现以前的版本包在 javax.validation 包下,Java 8之后在 jakarta.validation。
hibernate-validator
hibernate-validator 框架是另外一个针对 Bean Validation 规范的实现,它提供了 JSR 380 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。
使用 validator 进行请求参数校验实战
那 Spring Boot 项目中,Bean Validation 的实现框架怎么优雅地解决请求参数校验问题呢?
接下来,我们开始实战。我们将继续采用「接口常规校验案例」章节中的 UserDTO、AddressDTO,字段的约束一样。
新建 Spring Boot 项目,引入 spring-boot-start-web 依赖,Spring Boot 2.3.0 之后版本还需要引入 hibernate-validator,之前的版本已经包含 。
校验 @RequestBody 注解的参数
要校验使用 @RequestBody 注解的参数,需要 2 个步骤:
- 对参数对象的字段使用约束注解进行标注
- 在接口中对要校验的参数对象标注
@Valid或者@Validated
使用约束注解对字段进行标注
UserDTO
public class UserDTO { @NotBlank(message = "用户账号不能为空") @Size(min = 6, max = 11, message = "账号长度必须是6-11个字符") private String name; @Size(min = 6, max = 16, message = "密码长度必须是6-16个字符") private String password; @Range(min = 0, max = 2, message = "性别只能为 0:未知,1:男,2:女") private int sex; @NotNull(message = "地址信息不能为空") @Valid private AddressDTO address; // 省略 Getter/Setter} AddressDTO
public class AddressDTO { @NotBlank(message = "所在省份不能为空") private String province; @NotBlank(message = "所在城市不能为空") private String city; @NotBlank(message = "详细地址不能为空") private String detail; // 省略 Getter/Setter} 可以看到,我们在需要校验的字段上使用 @NotNull、@Size、@Range 等约束注解进行了标注,在 AddressDTO 上还使用了 @Valid,并且注解中定义了 message 等信息。
为对象类型参数添加 @Validated
现在再来看 UserController 中新建用户接口的处理(注意:这里是通过 Content-Type: application/json 提交的,请求参数需要在 @RequestBody 中获取)。我们在需要校验的 UserDTO 参数前添加 @Validated 注解。省略掉新增用户的逻辑之后,没有其他的显式校验的代码。
/** * 创建用户,通过 Content-Type: application/json 提交 * * Validator 校验失败将抛出 {@link MethodArgumentNotValidException} * * @param userDTO * @return */@PostMapping("/create-in-request-body")public UserDTO createInRequestBody(@Validated @RequestBody UserDTO userDTO) { // 通过 Validator 校验参数,开始处理用户插入等逻辑,操作成功以后响应 return userDTO;}验证校验结果
启动 Spring Boot 应用,用 Postman 调用请求,观察结果。

输入不符合要求的字段后,服务器返回了错误的结果。(经试验:Spring Boot 2.1.4.RELEASE 版本和 Spring Boot 2.3.3.RELEASE 版本输出结果不一样,前者输出还包含 errors 展示具体每个不符合校验规则的明细。)

在 Idea 的 Console 中可以看到如下日志,这说明不符合校验规则的参数已经被验证。
Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public io.ron.demo.validator.use.dto.UserDTO io.ron.demo.validator.use.controller.UserController.createInRequestBody(io.ron.demo.validator.use.dto.UserDTO) with 3 errors: [Field error in object 'userDTO' on field 'password': rejected value [123]; codes [Size.userDTO.password,Size.password,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [userDTO.password,password]; arguments []; default message [password],16,6]; default message [密码长度必须是6-16个字符]] [Field error in object 'userDTO' on field 'name': rejected value [lang1]; codes [Size.userDTO.name,Size.name,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [userDTO.name,name]; arguments []; default message [name],11,6]; default message [账号长度必须是6-11个字符]] [Field error in object 'userDTO' on field 'sex': rejected value [3]; codes [Range.userDTO.sex,Range.sex,Range.int,Range]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [userDTO.sex,sex]; arguments []; default message [sex],2,0]; default message [性别只能为 0:未知,1:男,2:女]] ]如果请求参数都满足条件,则能正确响应结果。
校验不使用 @RequestBody 注解的对象
有时我们也会编写这样的接口,接口方法中的对象类型参数不使用 @RequestBody 注解。使用 @RequestBody 注解的对象类型参数需要明确以 Content-Type: application/json 上传。而这种写法可以以 Content-Type: application/x-www-form-urlencoded 上传。
这样编写接口,校验方法与被 @RequestBody 注解的对象参数一样。
验证校验结果

在 Idea 的 Console 中可以看到如下日志:
2020-09-18 17:56:05.191 WARN 9734 --- [nio-9001-exec-9] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 2 errorsField error in object 'userDTO' on field 'name': rejected value [12345]; codes [Size.userDTO.name,Size.name,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [userDTO.name,name]; arguments []; default message [name],11,6]; default message [账号长度必须是6-11个字符]Field error in object 'userDTO' on field 'address.detail': rejected value [ ]; codes [NotBlank.userDTO.address.detail,NotBlank.address.detail,NotBlank.detail,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [userDTO.address.detail,address.detail]; arguments []; default message [address.detail]]; default message [详细地址不能为空]]请注意日志中的异常类型与 @RequestBody 注解的校验异常的区别。
这里报的异常类型是:
org.springframework.validation.BindException,而上一节中报的异常类型是:
org.springframework.web.bind.MethodArgumentNotValidException。在下一节中的情形,报的异常则是:
javax.validation.ConstraintViolationException。异常的处理我们将在下一篇文章中说明。请关注我的公众号:精进Java(ID:craft4j),第一时间获取知识动态。
校验 @PathVariable 与 @RequestParam 注解的参数
在真实项目中,不是所有的接口都接受对象类型的参数,如分页接口中的页码会使用 @RequestParam 注解;Restful 风格的接口会通过 @PathVariable 来获取资源 ID 等。这些参数无法通过上面的方法被 validator 校验。
要校验 @PathVariable 与 @RequestParam 注解的参数,需要 2 个步骤:
- 在要校验的接口类上标注
@Validated注解 - 在简单类型参数前标注
@PathVariable或@RequestParam
/** * 测试 @PathVariable 参数的校验 * * Validator 校验失败将抛出 {@link ConstraintViolationException} * * @param id * @return */@GetMapping("/user/{id}")public UserDTO retrieve(@PathVariable("id") @Min(value = 10, message = "id 必须大于 10") Long id) { return buildUserDTO("lfy", "qwerty", 1);}/** * 测试 @RequestParam 参数校验 * * 在方法上加 @Validated 无法校验 @RequestParam 与 @PathVariable * * 必须在类上 @Validated * * Validator 校验失败将抛出 {@link ConstraintViolationException} * * @param name * @param password * @param sex * @return */// @Validated@GetMapping("/validate")public UserDTO validate(@NotNull @Size(min = 6, max = 11, message = "账号长度必须是6-11个字符") @RequestParam("name") String name, @RequestParam("password") @Size(min = 6, max = 16, message = "密码长度必须是6-16个字符") String password, @RequestParam("sex") @Range(min = 0, max = 2, message = "性别只能为 0:未知,1:男,2:女") int sex) { return buildUserDTO(name, password, sex);}private UserDTO buildUserDTO(String name, String password, int sex) { UserDTO userDTO = new UserDTO(); userDTO.setName(name); userDTO.setPassword(password); userDTO.setSex(sex); return userDTO;}验证 @PathVariable 校验结果
输入小于 10 的 id,结果如下:

Idea 中 Console 报错误日志如下:
2020-09-18 17:23:55.744 ERROR 9734 --- [nio-9001-exec-7] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is javax.validation.ConstraintViolationException: retrieve.id: id 必须大于 10] with root causejavax.validation.ConstraintViolationException: retrieve.id: id 必须大于 10 at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:116) ~[spring-context-5.2.8.RELEASE.jar:5.2.8.RELEASE] ......验证 @RequestParam 校验结果
输入不满足要求的参数 name 和 password

Idea 中 Console 报错误日志如下:
2020-09-18 17:37:51.875 ERROR 9734 --- [nio-9001-exec-8] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is javax.validation.ConstraintViolationException: validate.password: 密码长度必须是6-16个字符, validate.name: 账号长度必须是6-11个字符] with root causejavax.validation.ConstraintViolationException: validate.password: 密码长度必须是6-16个字符, validate.name: 账号长度必须是6-11个字符 at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:116) ~[spring-context-5.2.8.RELEASE.jar:5.2.8.RELEASE] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE] ......分组校验
还有这样的场景,如在新建用户的接口中,用户的 ID 字段为空;而在更新用户的接口中,用户的 ID 字段则要求是必填。针对同一个用户实体对象,我们可以利用 validator 提供的分组校验。
分组校验的步骤如下:
- 定义分组接口。
- 对字段使用约束注解进行标注,使用 groups 参数进行分组。
- 用
@Validated标识参数,并设置 groups 参数。
普通分组
Update:我们定义一个 Update 分组接口
public interface Update {}UserDTO:对字段上的约束注解添加 groups 参数。下面对 id 的 @NotNull 添加了 Update 分组,对 name 字段的 @NotBlank 添加了 Update 分组及 validator 的默认 Default 分组。
@NotNull(message = "用户ID不能为空", groups = { Update.class })private Long id;@NotBlank(message = "用户账号不能为空", groups = { Update.class, Default.class })@Size(min = 6, max = 11, message = "账号长度必须是6-11个字符")private String name;UserController:在更新用户接口中对参数使用 @Validated 注解并设置 Update 分组。
@PutMapping("/update")public UserDTO update(@Validated({Update.class}) @RequestBoy UserDTO userDTO) { // 通过 Validator 校验参数,开始处理用户插入等逻辑,操作成功以后响应 return userDTO;}这里将只会对设置了分组为 Update 的约束进行校验,当 id 为空或者 name 为空或者空白的时候会报约束错误。当 id 与 name 均不为空时,即使 name 的长度不在 6-11 个字符之间,也不会校验。
组序列
除了按组指定是否验证之外,还可以指定组的验证顺序,前面组验证不通过的,后面组将不进行验证。
OrderedGroup:定义了校验组的顺序,Update 优先于 Default。
@GroupSequence({ Update.class, Default.class })public interface OrderedGroup {}UserController:在接口参数中 @Validated 注解设置参数 OrderedGroup.class。
@PostMapping("/ordered")public UserDTO ordered(@Validated({OrderedGroup.class}) @RequestBody UserDTO userDTO) { // 通过 Validator 校验参数,开始处理用户插入等逻辑,操作成功以后响应 return userDTO;}与普通分组中的案例结果不同,这里会优先校验 id 是否为空或者 name 是否为空或者空白,即 Update 分组的约束;Update 分组约束满足之后,还会进行其他参数的校验,因为其他参数都默认为 Default 分组。
hibernate-validator 的校验模式
从上面的案例中,细心的你可能已经发现了这样的现象:所有的参数都做了校验。实际上只要有一个参数校验不通过,我们就可以响应给用户。而 hibernate-validator 可以支持两种校验模式:
- 普通模式,默认是这种模式,该模式会校验完所有的属性,然后返回所有的验证失败信息
- 快速失败返回模式,这种模式下只要有一个参数校验失败就立即返回
开启快速失败返回模式
@Configurationpublic class ValidatorConfig { @Value("${hibernate.validator.fail_fast:false}") private boolean failfast; @Bean public Validator validator() { ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) .configure() // failFast 为 true 时,只要出现校验失败的情况,就立即结束校验,不再进行后续的校验。 .failFast(failfast) .buildValidatorFactory(); return validatorFactory.getValidator(); } @Bean public MethodValidationPostProcessor methodValidationPostProcessor() { MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor(); // 设置 validator 模式为快速失败返回 postProcessor.setValidator(validator()); return postProcessor; }}我们需要对 MethodValidationPostProcessor 设置开启快失败返回模式的 validator。而 validator 则只需设置 hibernate.validator.fail_fast 属性为 true。
再次运行 Spring Boot 项目,进行测试,我们会发现现在只要有一个参数校验失败,就立即返回了。
自定义约束实现
vaidation-api 与 hibernate-validator 提供的约束注解已经能够满足我们绝大多数的参数校验要求,但有时我们可能也需要使用自定义的 Validator 校验器。
自定义约束实现与使用包含如下步骤:
- 自定义约束注解
- 实现
ConstraintValidator来自定义校验逻辑
通用枚举类型约束
我们以自定义一个相对通用的枚举类型约束来演示。
自定义 @EnumValue 枚举指约束注解
enumClass 标识字段取值对应哪个枚举类型,enumMethod 则是需要枚举类定义一个用于验证取值是否有效的验证方法,如果为空的话,我们会默认提供处理参数为整型与字符串型的情况。需要在注解上使用 @Constraint(validatedBy) 来设置具体使用的校验器。
@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })@Retention(RetentionPolicy.RUNTIME)@Constraint(validatedBy = EnumValue.EnumValidator.class)public @interface EnumValue { String message() default "无效的枚举值"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; Class<? extends Enum<?>> enumClass(); String enumMethod() default "";}编写 EnumValidator 来自定义校验逻辑
class EnumValidator implements ConstraintValidator<EnumValue, Object> { private Class<? extends Enum<?>> enumClass; private String enumMethod; @Override public void initialize(EnumValue enumValue) { enumMethod = enumValue.enumMethod(); enumClass = enumValue.enumClass(); } @Override public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) { if (value == null) { return Boolean.TRUE; } if (enumClass == null) { return Boolean.TRUE; } Class<?> valueClass = value.getClass(); if (enumMethod == null || enumMethod.equals("")) { String valueClassName = valueClass.getCanonicalName(); // 处理参数可以转为枚举值 ordinal 的情况 if (valueClassName.equals("java.lang.Integer")) { return enumClass.getEnumConstants().length > (Integer) value; } // 处理参数为枚举名称的情况 else if (valueClassName.equals("java.lang.String")) { return Arrays.stream(enumClass.getEnumConstants()).anyMatch(e -> e.toString().equals(value)); } throw new RuntimeException(String.format("A static method to valid enum value is needed in the %s class", enumClass)); } // 枚举类自定义取值校验 try { Method method = enumClass.getMethod(enumMethod, valueClass); if (!Boolean.TYPE.equals(method.getReturnType()) && !Boolean.class.equals(method.getReturnType())) { throw new RuntimeException(String.format("%s method return is not boolean type in the %s class", enumMethod, enumClass)); } if (!Modifier.isStatic(method.getModifiers())) { throw new RuntimeException(String.format("%s method is not static method in the %s class", enumMethod, enumClass)); } Boolean result = (Boolean) method.invoke(null, value); return result == null ? false : result; } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new RuntimeException(e); } catch (NoSuchMethodException | SecurityException e) { throw new RuntimeException(String.format("This %s(%s) method does not exist in the %s", enumMethod, valueClass, enumClass), e); } }}实现 ConstraintValidator 的两个方法 initialize、isValid,一个是初始化参数的方法,另一个就是校验逻辑的方法。
在校验方法中,当约束注解没有定义 enumMethod 时,我们根据传入需要校验的参数提供整型与字符型的两种默认校验,可以仔细看源码第 23-34 行。从 37-54 行的代码可以看到,除开上面的 2 种情况下,枚举类型需要提供一个自定义的校验方法。
在项目中使用
Gender:我们定义一个表示性别的枚举。这里我们编写了一个判断枚举取值是否有效的静态方法 isValid()。
public enum Gender { UNKNOWN, MALE, FEMALE; /** * 判断取值是否有效 * * @param val * @return */ public static boolean isValid(Integer val) { return Gender.values().length > val; }}UserDTO:给 sex 字段的设置 @EnumValue 约束
@Range(min = 0, max = 2, message = "性别只能为 0:未知,1:男,2:女")@EnumValue(enumClass = Gender.class)private int sex;接下来就是运行程序,发请求观察校验结果了。
总结
前面我们从一个常规校验案例开始,说明了 Bean Validation 规范及其实现,并从实战角度出发介绍了各种场景下的校验,包括:
- 使用
@RequestBody和不使用@RequestBody注解的对象类型参数 - 使用
@RequestParam和@PathVariable注解的简单类型参数 - 分组校验
- 快速失败返回校验模式
- 自定义约束校验
总体而言,使用 validator 能够极大的方便请求参数的校验,简化校验相关的实现代码。但是,细心的读者也发现了,本文中所有的接口当有参数校验失败时,都是报了异常,返回的响应中直接报 400 或者 500 的错误。响应不直观,不规范,给到前端也无法方便高效的处理。
在接下来的文章中,我将继续为大家带来全局异常处理、统一响应结构的知识与实战。
文中涉及的代码已经开源在我的 Github 仓库 ron-point 中。如果觉得不错请点个 star,欢迎一起讨论和交流。
原文转载:http://www.shaoqun.com/a/488097.html
let go:https://www.ikjzd.com/w/825
google趋势:https://www.ikjzd.com/w/397
一淘网比价平台:https://www.ikjzd.com/w/1698
团队新来了个校招实习生静静,相互交流后发现竟然是我母校同实验室的小学妹,小学妹很热情地认下了我这个失散多年的大湿哥,后来...小学妹:大湿哥,咱们项目里的Controller怎么都看不到参数校验处理的代码呀?但是程序运行起来,看到有是有校验的?大湿哥:哦哦,静静,你看到Controller类和方法上的@Validated,还有其他参数的@NotBlank、@Size这些注解了吗?小学妹:看到了,你
李群:https://www.ikjzd.com/w/1767
mail.ru:https://www.ikjzd.com/w/2232
苏州哪里夜景好看?:http://tour.shaoqun.com/a/5923.html
"南京路上好八连"雕塑亮相上海南京路 :http://tour.shaoqun.com/a/16861.html
亚马逊跟卖投诉:卖家投诉跟卖警告信模板和投诉跟卖方法:https://www.ikjzd.com/home/6803
没有评论:
发表评论