spring下的数据校验
前言
数据的校验是web后端(服务端)一个不可或缺的功能,前端的js校验可以涵盖大部分的校验规则,如生日格式,邮箱格式校验等。但是为了避免用户绕过前端的js(浏览器),使用http工具直接向后端请求一些违法数据,web后端的数据校验也是必要的,可以防止脏数据落到数据库中。
概念
Bean Validation
是Java定义的一套基于注解的数据校验规范,目前已经从JSR 303
的1.0版本升级到JSR 349
的1.1版本,再到JSR 380
的2.0版本(2.0完成于2017.08),已经经历了三个版本。规范注解如:@Null
,@NotNull
,@Pattern
,javax.validation.constraints
包下,只提供规范不提供实现。
Hibernate Validator
则是Bean Validation
的参考实现,它提供了Bean Validation
规范中所有内置constraint的实现,除此之外还有一些附加的constraint,如@Email,@Length,@Range等,位于org.hibernate.validator.constraints
包下。
Spring validation
对hibernate-validation
进行了二次封装,显示校验validated bean
时,你可以使用Spring validation
或者hibernate validation
,而spring validation
另一个特性,便是在springmvc
模块中添加了自动检验,并将校验信息封装进了特定的类中,位于org.springframework.validation
包下。
检验类型
注解 | 说明 |
---|---|
@Null | 验证对象是否为空 |
@NotNull | 验证对象是否不为null, 无法查检长度为0的字符串 |
@NotBlank | 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格 |
@NotEmpty | 检查约束元素是否为NULL或者是EMPTY |
@AssertTrue | 验证 Boolean 对象是否为 true |
@AssertFalse | 验证 Boolean 对象是否为 false |
@Size(min=, max=) | 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内 |
@Length(min=, max=) | 验证字符串长度是否在给定的范围之内 |
@Past | 验证 Date 和 Calendar 对象是否在当前时间之前 |
@Future | 验证 Date 和 Calendar 对象是否在当前时间之后 |
@Pattern | 验证 String 对象是否符合正则表达式的规则 |
@Min | 验证 Number 和 String 对象是否大等于指定的值 |
@Max | 验证 Number 和 String 对象是否小等于指定的值 |
@DecimalMax | 被标注的值必须不大于约束中指定的最大值. 这个约束的参数是一个通过BigDecimal定义的最大值的字符串表示.小数存在精度 |
@DecimalMin | 被标注的值必须不小于约束中指定的最小值. 这个约束的参数是一个通过BigDecimal定义的最小值的字符串表示.小数存在精度 |
@Digits | 验证 Number 和 String 的构成是否合法 |
@Digits(integer=,fraction=) | 验证字符串是否是符合指定格式的数字,interger指定整数精度,fraction指定小数精度 |
@Range(min=, max=) | 验证值是不是在该范围内 |
@Valid | 递归的对关联对象进行校验, 如果关联对象是个集合或者数组,那么对其中的元素进行递归校验,如果是一个map,则对其中的值部分进行校验.(是否进行递归验证) |
@CreditCardNumber | 信用卡验证 |
@Email | 验证是否是邮件地址,如果为null,不进行验证,算通过验证 |
检验实现
创建工程
- 选中idea菜单
File
,从弹出的子菜单中选择New
,再从子菜单中选择并点击Project...
,如下图
- 经过第一步的操作,会弹出如下图的界面,选择
Spring Initializer
,默认Project SDK
和Initializr Service URL
的值,点击next
按钮。
- 经过第二步的操作,会弹出如下界面,设置
Group
和Artifact
,点击Next
按钮
- 经过第三步,会弹出如下界面,选择
Developer Tools
,选中Lombok
,选择Web
,选中Srping Web Starter
,点击Next
按钮 - 经过第四步操作,会弹出如下界面,设置
Project Name
和Project location
,点击finish
按钮 ,到此我们的项目创建完毕。接下来开始写代码
引入swagger
- 在pom.xml文件中加入如下依赖
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-bean-validators</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-bean-validators</artifactId>
<version>2.6.1</version>
</dependency>
- 在
com.study.web
包下新建config
包,在com.study.web.config
包下,新建SwaggerConfig
类,代码如下:
package com.study.web.config;
import static springfox.documentation.builders.PathSelectors.ant;
import com.google.common.base.Predicates;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
/**
* swagger工具配置
*/
@Configuration
@EnableSwagger2
public class SwaggerConfig extends WebMvcConfigurerAdapter {
@Value("${swaggerUrl}")
private String swaggerUrl;
/**
* SpringBoot默认已经将classpath:/META-INF/resources/和classpath:/META-INF/resources/webjars/映射
* 所以该方法不需要重写,如果在SpringMVC中,可能需要重写定义(我没有尝试) 重写该方法需要 extends WebMvcConfigurerAdapter
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
@Bean
public Docket restApi() {
return new Docket(DocumentationType.SWAGGER_2)
.host(swaggerUrl)
//.apiInfo(apiInfo())
.select()
.paths(Predicates.and(ant("/**"), Predicates.not(ant("/error"))))
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Swagger Petstore")
.description("Petstore API Description")
.contact(new Contact("leongfeng", "http:/test-url.com", "leongfeng@163.com"))
.license("Apache 2.0")
.licenseUrl("http://www.apache.org/licenses/LICENSE-2.0.html")
.version("1.0.0")
.build();
}
}
新建UserController
类
- 在com.study.web.controller包下,新增
UserController
类,代码如下:
package com.study.web.controller;
import com.study.web.domain.vo.SaveUserVo;
import com.study.web.domain.vo.UpdateUserVo;
import com.study.web.domain.vo.UserVo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
/**
* @author ZOUZHIHUI
* @Date 2019-06-22
*/
@RestController("user")
@Api(value = "UserController", description = "用户接口")
public class UserController {
@RequestMapping(value = "save", method = RequestMethod.POST)
@ApiOperation(value = "save", nickname = "保存用户信息")
public String save(@Validated @RequestBody SaveUserVo userVo) {
return "success";
}
}
SaveUserVo
类如下:
package com.study.web.domain.vo;
import javax.validation.constraints.NotEmpty;
import lombok.Data;
/**
* @author ZOUZHIHUI
* @Date 2019-06-22
*/
@Data
public class SaveUserVo {
@NotEmpty(message = "用户名不能为空!")
private String username;
@NotEmpty(message = "密码不能为空!")
private String password;
}
运行系统
- 配置application.properties文件
server.port=8989
server.servlet.context-path=/study-web
swaggerUrl: localhost:8989
- 找到
StudyWebApplication
类,右键debug运行,服务启动完之后,在浏览器上输入如下地址:http://localhost:8989/study-web/swagger-ui.html
,能正常访问表示服务没问题。
测试
- 访问该地址
http://localhost:8989/study-web/swagger-ui.html
,找到user-controller
的save
接口,按下图,配置参数,点击Try it out
按钮
- 会得到如下的响应结果,表明验证功能生效
{
"timestamp": "2019-06-26T02:45:25.879+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"NotEmpty.saveUserVo.username",
"NotEmpty.username",
"NotEmpty.java.lang.String",
"NotEmpty"
],
"arguments": [
{
"codes": [
"saveUserVo.username",
"username"
],
"arguments": null,
"defaultMessage": "username",
"code": "username"
}
],
"defaultMessage": "用户名不能为空!",
"objectName": "saveUserVo",
"field": "username",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotEmpty"
},
{
"codes": [
"NotEmpty.saveUserVo.password",
"NotEmpty.password",
"NotEmpty.java.lang.String",
"NotEmpty"
],
"arguments": [
{
"codes": [
"saveUserVo.password",
"password"
],
"arguments": null,
"defaultMessage": "password",
"code": "password"
}
],
"defaultMessage": "密码不能为空!",
"objectName": "saveUserVo",
"field": "password",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotEmpty"
}
],
"message": "Validation failed for object='saveUserVo'. Error count: 2",
"path": "/study-web/save"
}
spring校验功能源码解析
- 参数验证调用流程,如下图,主要介绍几个重要流程
HandlerMethodArgumentResolverComposite
类,该类实现了HandlerMethodArgumentResolver
接口,该类存放了spring提供的所有参数解析器类。会根据请求参数来找到唯一的参数解析器类,本例子中是RequestResponseBodyMethodProcessor
。也就是说不同从参数会有不同的解析器类。列表如下:
类名 | 描述 |
---|---|
RequestParamMethodArgumentResolver | 针对被 @RequestParam 注解修饰, 但类型不是 Map, 或类型是 Map, 并且 @RequestParam 中指定 name, 一般通过 MultipartHttpServletRequest |
RequestParamMapMethodArgumentResolver | 针对被 @RequestParam注解修饰, 且参数类型是 Map 的, 且 @RequestParam 中没有指定 name, 从 HttpServletRequest 里面获取所有请求参数, 最后封装成 LinkedHashMap |
PathVariableMethodArgumentResolver | 解决被注解 @PathVariable 注释的参数 <- 这个注解对应的是 uri 中的数据, 在解析 URI 中已经进行解析好了 <- 在 RequestMappingInfoHandlerMapping.handleMatch -> getPathMatcher().extractUriTemplateVariables |
PathVariableMapMethodArgumentResolver | 针对被 @PathVariable 注解修饰, 并且类型是 Map的, 且 @PathVariable.value == null, 从 HttpServletRequest 中所有的 URI 模版变量 (PS: URI 模版变量的获取是通过 RequestMappingInfoHandlerMapping.handleMatch 获取) |
MatrixVariableMethodArgumentResolver | 针对被 @MatrixVariable 注解修饰的参数起作用, 从 HttpServletRequest 中获取去除 ; 的 URI Template Variables 获取数据 |
MatrixVariableMapMethodArgumentResolver | 针对被 @MatrixVariable 注解修饰, 并且类型是 Map的, 且 MatrixVariable.name == null, 从 HttpServletRequest 中获取 URI 模版变量 <-- 并且是去除 ; |
ServletModelAttributeMethodProcessor | 针对被@ModeAttribute注解修饰的 |
RequestResponseBodyMethodProcessor | 解决被 @RequestBody 注释的方法参数 <- 其间是用 HttpMessageConverter 进行参数的转换 |
RequestPartMethodArgumentResolver | 参数被 @RequestPart 修饰, 参数是 MultipartFile |
RequestHeaderMethodArgumentResolver | 针对 参数被 RequestHeader 注解, 并且 参数不是 Map 类型, 数据通过 HttpServletRequest.getHeaderValues(name) 获取 |
RequestHeaderMapMethodArgumentResolver | 解决被 @RequestHeader 注解修饰, 并且类型是 Map 的参数, HandlerMethodArgumentResolver会将 Http header 中的所有 name <–> value 都放入其中 |
ServletCookieValueMethodArgumentResolver | 针对被 @CookieValue 修饰, 通过 HttpServletRequest.getCookies 获取对应数据 |
ExpressionValueMethodArgumentResolver | 针对被 @Value 修饰, 返回 ExpressionValueNamedValueInfo |
SessionAttributeMethodArgumentResolver | 针对 被 @SessionAttribute 修饰的参数起作用, 参数的获取一般通过 HttpServletRequest.getAttribute(name, RequestAttributes.SCOPE_SESSION) |
RequestAttributeMethodArgumentResolver | 针对 被 @RequestAttribute 修饰的参数起作用, 参数的获取一般通过 HttpServletRequest.getAttribute(name, RequestAttributes.SCOPE_REQUEST) |
ServletRequestMethodArgumentResolver | 支持 WebRequest, ServletRequest, MultipartRequest, HttpSession, Principal, InputStream, Reader, HttpMethod, Locale, TimeZone, 数据通过 HttpServletRequest 获取 |
ServletResponseMethodArgumentResolver | 支持 ServletResponse, OutputStream, Writer 类型, 数据的获取通过 HttpServletResponse |
HttpEntiyMethodProcessor | 针对 HttpEntity,RequestEntity 类型的参数进行参数解决, 将 HttpServletRequest 里面的数据转换成 HttpEntity |
RedirectAttributesMethodArgumentResolver | 针对 RedirectAttributes及其子类的参数 的参数解决器, 主要还是基于 NativeWebRequest && DataBinder (通过 dataBinder 构建 RedirectAttributesModelMap) |
ModelMethodProcessor | 针对 Model 及其子类的参数, 数据的获取一般通过 ModelAndViewContainer.getModel() |
MapMethodProcessor | 针对参数是 Map, 数据直接从 ModelAndViewContainer 获取 Model |
ErrorsMethodArgumentResolver | 参数是Errors |
SessionStatusMethodArgumentResolver | 支持参数类型是 SessionStatus, 直接通过 ModelAndViewContainer 获取 SessionStatus |
UriComponentsBuilderMethodArgumentResolver | 支持参数类型是 UriComponentsBuilder, 直接通过 ServletUriComponentsBuilder.fromServletMapping(request) 构建对象 |
参考:
spring mvc 数据绑定
SpringMVC 4.3 源码分析之 HandlerMethodArgumentResolver
RequestResponseBodyMethodProcessor
来解析具体参数,
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
parameter = parameter.nestedIfOptional();
// 从request中读取参数,并转成对应的参数对象
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
String name = Conventions.getVariableNameForParameter(parameter);
if (binderFactory != null) {
// 创建WebDataBinder,此处默认创建ExtendedServletRequestDataBinder
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
if (arg != null) {
// 执行参数验证,如果验证失败,会把错误信息封装到binder中
validateIfApplicable(binder, parameter);
// 判断binder中是否有验证失败的信息,如果有,则抛出异常
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);
}
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
// 获取参数的注解
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation ann : annotations) {
// 获取Validated注解
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
// 判断是否有Validated注解或者是以Valid开头的注解,匹配@Valid注解
if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
// 获取注解中的值,主要是用于group分组检验
Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
// 调用ExtendedServletRequestDataBinder类的validate()方法
binder.validate(validationHints);
break;
}
}
}
ExtendedServletRequestDataBinder
,该方法遍历所有可用的验证器,并调用每个验证器的validate方法
public void validate(Object... validationHints) {
Object target = getTarget();
Assert.state(target != null, "No target to validate");
BindingResult bindingResult = getBindingResult();
// Call each validator with the same binding result
for (Validator validator : getValidators()) {
// 判断是否有分组验证功能,如果有则走下面的验证
if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator) {
((SmartValidator) validator).validate(target, bindingResult, validationHints);
}
else if (validator != null) { // 没有分组验证,则走下面的验证
validator.validate(target, bindingResult);
}
}
}
ValidatorAdapter
public void validate(Object target, Errors errors) {
this.target.validate(target, errors);
}
SpringValidatorAdapter
类中会调用hibernate-validate
中的ValidatorImpl
来实现具体的验证,并解析返回验证信息,放入Errors
中,关于ValidatorImpl
的具体验证规则,后续再分析
public void validate(Object target, Errors errors) {
if (this.targetValidator != null) {
processConstraintViolations(this.targetValidator.validate(target), errors);
}
}
protected void processConstraintViolations(Set<ConstraintViolation<Object>> violations, Errors errors) {
for (ConstraintViolation<Object> violation : violations) {
String field = determineField(violation);
FieldError fieldError = errors.getFieldError(field);
if (fieldError == null || !fieldError.isBindingFailure()) {
try {
ConstraintDescriptor<?> cd = violation.getConstraintDescriptor();
String errorCode = determineErrorCode(cd);
Object[] errorArgs = getArgumentsForConstraint(errors.getObjectName(), field, cd);
if (errors instanceof BindingResult) {
// Can do custom FieldError registration with invalid value from ConstraintViolation,
// as necessary for Hibernate Validator compatibility (non-indexed set path in field)
BindingResult bindingResult = (BindingResult) errors;
String nestedField = bindingResult.getNestedPath() + field;
if (nestedField.isEmpty()) {
String[] errorCodes = bindingResult.resolveMessageCodes(errorCode);
ObjectError error = new ObjectError(
errors.getObjectName(), errorCodes, errorArgs, violation.getMessage()) {
@Override
public boolean shouldRenderDefaultMessage() {
return requiresMessageFormat(violation);
}
};
error.wrap(violation);
bindingResult.addError(error);
}
else {
Object rejectedValue = getRejectedValue(field, violation, bindingResult);
String[] errorCodes = bindingResult.resolveMessageCodes(errorCode, field);
FieldError error = new FieldError(errors.getObjectName(), nestedField,
rejectedValue, false, errorCodes, errorArgs, violation.getMessage()) {
@Override
public boolean shouldRenderDefaultMessage() {
return requiresMessageFormat(violation);
}
};
error.wrap(violation);
bindingResult.addError(error);
}
}
else {
// got no BindingResult - can only do standard rejectValue call
// with automatic extraction of the current field value
errors.rejectValue(field, errorCode, errorArgs, violation.getMessage());
}
}
catch (NotReadablePropertyException ex) {
throw new IllegalStateException("JSR-303 validated property '" + field +
"' does not have a corresponding accessor for Spring data binding - " +
"check your DataBinder's configuration (bean property versus direct field access)", ex);
}
}
}
}
分组检验
新建注解类
- 在
com.study.web.group
包下,新增Save
和Update
两个接口类,主要对应新增和修改两个接口
package com.study.web.group;
/**
* @author ZOUZHIHUI
* @Date 2019-06-27
*/
public interface Save {
}
package com.study.web.group;
/**
* @author ZOUZHIHUI
* @Date 2019-06-27
*/
public interface Update {
}
在UserController
新增修改和新增接口
- 在新增方法中使用
@Validate
注解,value
为{Save.class}
。在修改方法中使用@Validate
注解,value
为{Update.class}
@RequestMapping(value = "save2", method = RequestMethod.POST)
@ApiOperation(value = "save2", nickname = "保存用户信息2")
public String save2(@Validated({Save.class}) @RequestBody UserVo userVo) {
return "success";
}
@RequestMapping(value = "update2", method = RequestMethod.POST)
@ApiOperation(value = "update2", nickname = "更新用户信息2")
public String update2(@Validated({Update.class}) @RequestBody UserVo userVo) {
return "success";
}
UserVo
代码如下, 当新增时,username
和password
参数不能为空;当修改时,id
和username
不能为空。
package com.study.web.domain.vo;
import com.study.web.group.Save;
import com.study.web.group.Update;
import javax.validation.constraints.NotEmpty;
import lombok.Data;
/**
* @author ZOUZHIHUI
* @Date 2019-06-22
*/
@Data
public class UserVo {
@NotNull(groups = {Update.class})
private Integer id;
@NotEmpty(groups = {Save.class, Update.class})
private String username;
@NotEmpty(groups = {Save.class})
private String password;
}
运行&测试
- 访问该地址
http://localhost:8989/study-web/swagger-ui.html
,找到user-controller
的save2
接口,按下图,配置参数,点击Try it out
按钮 - 会得到如下的响应结果,说明分组验证功能成功,新增是
username
和password
不能为空
{
"timestamp": "2019-06-27T02:32:22.815+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"NotEmpty.userVo.password",
"NotEmpty.password",
"NotEmpty.java.lang.String",
"NotEmpty"
],
"arguments": [
{
"codes": [
"userVo.password",
"password"
],
"arguments": null,
"defaultMessage": "password",
"code": "password"
}
],
"defaultMessage": "不能为空",
"objectName": "userVo",
"field": "password",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotEmpty"
},
{
"codes": [
"NotEmpty.userVo.username",
"NotEmpty.username",
"NotEmpty.java.lang.String",
"NotEmpty"
],
"arguments": [
{
"codes": [
"userVo.username",
"username"
],
"arguments": null,
"defaultMessage": "username",
"code": "username"
}
],
"defaultMessage": "不能为空",
"objectName": "userVo",
"field": "username",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotEmpty"
}
],
"message": "Validation failed for object='userVo'. Error count: 2",
"path": "/study-web/save2"
}
- 访问该地址
http://localhost:8989/study-web/swagger-ui.html
,找到user-controller
的update2
接口,按下图,配置参数,点击Try it out
按钮
- 会得到如下的响应结果,说明分组验证功能成功,修改是是
id
和username
不能为空
{
"timestamp": "2019-06-27T02:40:20.497+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"NotNull.userVo.id",
"NotNull.id",
"NotNull.java.lang.Integer",
"NotNull"
],
"arguments": [
{
"codes": [
"userVo.id",
"id"
],
"arguments": null,
"defaultMessage": "id",
"code": "id"
}
],
"defaultMessage": "不能为null",
"objectName": "userVo",
"field": "id",
"rejectedValue": null,
"bindingFailure": false,
"code": "NotNull"
},
{
"codes": [
"NotEmpty.userVo.username",
"NotEmpty.username",
"NotEmpty.java.lang.String",
"NotEmpty"
],
"arguments": [
{
"codes": [
"userVo.username",
"username"
],
"arguments": null,
"defaultMessage": "username",
"code": "username"
}
],
"defaultMessage": "不能为空",
"objectName": "userVo",
"field": "username",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotEmpty"
}
],
"message": "Validation failed for object='userVo'. Error count: 2",
"path": "/study-web/update2"
}
自定义检验
手动检验
关于自定义校验功能和在代码层面手动调用校验功能,请参考西面的连接:
使用spring validation完成数据后端校验