数据的校验是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,不进行验证,算通过验证 |
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
按钮 ,到此我们的项目创建完毕。接下来开始写代码
io.springfox
springfox-swagger2
2.6.1
io.springfox
springfox-swagger-ui
2.6.1
io.springfox
springfox-bean-validators
2.6.1
io.springfox
springfox-bean-validators
2.6.1
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", "[email protected]"))
.license("Apache 2.0")
.licenseUrl("http://www.apache.org/licenses/LICENSE-2.0.html")
.version("1.0.0")
.build();
}
}
UserController
类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;
}
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"
}
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> violations, Errors errors) {
for (ConstraintViolation
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完成数据后端校验